Custom Composite Controls
In Chapter 4, we saw how binary custom controls render custom HTML to the browser. The factor distinguishing this kind of control most is that these controls override the Render method. Remember, the System.Web.UI.Page class manages a list of server-side controls. When ASP.NET asks the whole page to render, it goes to each control on the page and asks it to render. In the case of a rendering control, the control simply pushes some text into the stream bound for the browser. Likewise, when the page rendering mechanism hits a composite style control, the composite control walks its list of child controls, asking each one to render—just as the Page walks its own list of controls.
Composite controls may contain as many children as memory will accommodate, and the controls may be nested as deeply as necessary. Of course, there's a practical limit to the number and depth of the child controls. Adding too many controls or nesting them too deeply will add complexity to a page, and it may become unsightly.
In Chapter 4, we created a control that checked for palindromes. When the control's Text property was set to a palindrome, the control rendered the palindrome in blue text, added it to an ArrayList, and then rendered the contents of the palindrome collection as a table. Let's build the same control again—however, this time it will be a composite control.
The Palindrome Checker as a Composite Custom Control
Open the ControlORama project. Highlight the CustomControlLib project in the Solution Explorer. Right-click on the project node and select Add New Item. Create a new class and name the source file PalindromeCheckerCompositeControl.cs. Use the Web Custom Control template.
After Visual Studio creates the code, do the following:
Edit the code to change the derivation from WebControl to CompositeControl.Deriving from the CompositeControl also adds the INamingContainer interface to the derivation list. (INamingContainer is useful to help ASP.NET manage unique IDs for the control's children.)
Add an event handler that the host page may use to listen for palindrome detections.
Remove the Render method.
Add four member variables, a TextBox, a Button, a Label, and a LiteralControl.
The code should look something like this when you're finished:
public class PalindromeCheckerCompositeControl : CompositeControl { protected TextBox textboxPalindrome; protected Button buttonCheckForPalindrome; protected Label labelForTextBox; protected Table tablePalindromes; protected LiteralControl literalcontrolPalindromeStatus; public event EventHandler PalindromeFound; … // Render method removed. }
Leave the Text property intact. We'll still need it in this control.
The control is very much like the one in Chapter 4. However, this version will include the palindrome TextBox, the Button to invoke palindrome checking, and will contain a literal control to display whether or not the current property is a palindrome.
Borrow the StripNonAlphanumerics and CheckForPalindrome methods from the PalindromeCheckerRenderedControl:
protected string StripNonAlphanumerics(string str) { string strStripped = (String)str.Clone(); if (str != null) { char[] rgc = strStripped.ToCharArray(); int i = 0; foreach (char c in rgc) { if (char.IsLetterOrDigit(c)) { i++; } else { strStripped = strStripped.Remove(i, 1); } } } return strStripped; } protected bool CheckForPalindrome() { if (this.Text != null) { String strControlText = this.Text; String strTextToUpper = null; strTextToUpper = Text.ToUpper(); strControlText = this.StripNonAlphanumerics(strTextToUpper); char[] rgcReverse = strControlText.ToCharArray(); Array.Reverse(rgcReverse); String strReverse = new string(rgcReverse); if (strControlText == strReverse) { return true; } else { return false; } } else { return false; } }
Add an event handler to be applied to the Button (which we'll install on the page in just a minute). Because this is a binary control without designer support, you'll need to add the event handler using the text wizard (that is, you'll need to type it by hand).
public void OnCheckPalindrome(Object o, System.EventArgs ea) { this.Text = this.textboxPalindrome.Text; this.CheckForPalindrome(); }
This next part is what really distinguishes composite controls from rendered controls. Add an override for the CreateChildControls method. In the method, you'll need to create each UI element by hand, set the properties you want appearing in the control, and add the individual control to the composite control's list of controls.
protected override void CreateChildControls() { labelForTextBox = new Label(); labelForTextBox.Text = "Enter a palindrome: "; this.Controls.Add(labelForTextBox); textboxPalindrome = new TextBox(); this.Controls.Add(textboxPalindrome); Controls.Add(new LiteralControl("<br/>")); buttonCheckForPalindrome = new Button(); buttonCheckForPalindrome.Text = "Check for Palindrome"; buttonCheckForPalindrome.Click += new EventHandler(OnCheckPalindrome); this.Controls.Add(buttonCheckForPalindrome); Controls.Add(new LiteralControl("<br/>"")); literalcontrolPalindromeStatus = new LiteralControl(); Controls.Add(literalcontrolPalindromeStatus); Controls.Add(new LiteralControl("<br/>")); this.tablePalindromes = new Table(); this.Controls.Add(tablePalindromes); }
While the code listed above is pretty straightforward, a couple of lines deserve special note. First is the use of the LiteralControl to render the line breaks. Remember—every element on the page (or in this case the control) will be rendered using a server-side control. If you want any literal text rendered as part of your control, you need to package it in a server-side control. The job of a LiteralControl is to take the contents (the Text property) and simply render it to the outgoing stream.
The second thing to notice is how the event handler is hooked to the Button using a delegate. This is usually handled in Visual Studio by clicking on a UI element in the designer. However, because there's no designer support here, the event hookup needs to be handled manually.
Show the palindrome status whenever the Text property is set. Modify the Text property's setter so that it checks for a palindrome and renders the result in the LiteralControl. It should also raise the PalindromeFound event.
public string Text { get { return text; } set { text = value; if (this.CheckForPalindrome()) { if (PalindromeFound != null) { PalindromeFound(this, EventArgs.Empty); } literalcontrolPalindromeStatus.Text = "This is a palindrome <br><FONT size=5 color=blue><B>" + text + "</B> </FONT>"; } else { literalcontrolPalindromeStatus.Text = "This is NOT a palindrome <br><FONT size=5 color=red><B>" + text + "</B> </FONT>"; } } }
Show the palindromes in a table, just as the rendered version of this control did. First, add an ArrayList and a Table Table control to the PalindromeCheckerCompositeControl class.
public class PalindromeCheckerCompositeControl : Control, INamingContainer { protected Table tablePalindromes; protected ArrayList alPalindromes; //… }
Add a method to build the palindrome table based on the contents of the ArrayList. Check to see if the array list is stored in the ViewState. If it's not, then create a new one. Iterate through the palindrome collection and add a TableRow and a TableCell to the table for each palindrome found.
protected void BuildPalindromesTable() { this.alPalindromes = (ArrayList)this.ViewState["palindromes"]; if (this.alPalindromes != null) { foreach (string s in this.alPalindromes) { TableCell tableCell = new TableCell(); tableCell.BorderStyle = BorderStyle.Double; tableCell.BorderWidth = 3; tableCell.Text = s; TableRow tableRow = new TableRow(); tableRow.Cells.Add(tableCell); this.tablePalindromes.Rows.Add(tableRow); } } }
Update the Text property's setter to manage the table. Add palindromes to the ArrayList as they're found, and build the palindrome table each time the text is changed.
public string Text { get { return text; } set { text = value; this.alPalindromes = (ArrayList)this.ViewState["palindromes"]; if (this.alPalindromes == null) { this.alPalindromes = new ArrayList(); } if (this.CheckForPalindrome()) { if (PalindromeFound != null) { PalindromeFound(this, EventArgs.Empty); } alPalindromes.Add(text); literalcontrolPalindromeStatus.Text = "This is a palindrome <br><FONT size=5 color=blue><B>" + text + "</B> </FONT>""; } else { literalcontrolPalindromeStatus.Text = "This is NOT a palindrome <br><FONT size=5 color=red><B>" + text + "</B> </FONT>"; } this.ViewState.Add("palindromes", alPalindromes); this.BuildPalindromesTable(); } }
Build the project and add the User control to the ControlORama UsePalindromeCheckerControls.aspx page. You can pick up the User control directly from the toolbox and drop it on to the page. When you run the page, it will check for palindromes and keep a record of the palindromes that have been found, like so (tracing is turned on in this example so we can see the control tree a bit later on):
With tracing turned on, you can look further down and see the control tree. Notice how the PalindromeCheckerCompositeControl acts as a main node on the tree, and that the composite control's child controls are shown under the PalindromeCheckerCompositeControl node.
When you type palindromes and click the button, the control will detect them. The control displays the current Text property in red if it's not a palindrome, and in blue if it is a palindrome. You can also see the table rendering, showing the currently found palindromes.
The palindrome checker is a good example of a binary composite control. The composite control lives entirely within the CustomcontrolLib assembly and does not have any designer support. Here's an alternative to coding a composite control entirely by hand—the second way to create composite controls is via a User control.