- Techniques for Creating Reusable Content
- Building a ComboBox User Control
- Using the ComboBox Control
- Populating the ComboBox Control
- Summary
Building a ComboBox User Control
In the rest of this chapter, you'll see how to build a user control that implements some useful features you can make available in the pages of a Web application. You'll build the ComboBox control you saw earlier in this chapter and then see how it can be used in an ASP.NET page just as a native .NET Web Forms control would be used.
The combo box style of control is one of the most significant omissions from the standard set of controls that are implemented in a Web browser. There is no single HTML element you can use to create one, so you have to build up the complete interface to represent the features you want, using separate HTML control elements. The first step is to consider the requirements for the control and the HTML you will have to generate to produce the final effect you want in a Web page.
Design Considerations
It's usual for a combo box control to offer two modes of operation. The simplest is a combination of a text box and a list control, linked together so that a user can type a value in the text box or select a value from the list. Typing in the text box automatically scrolls and selects the first value in the list that matches the text in the text box, whereas selecting from a list places that value into the text box (see Figure 5.7).
Figure 5.7 A standard ComboBox control, showing a user typing in a value and selecting from the list.
The easiest way to create this kind of output in a Web browser is to use a single-cell table to restrain the two controls and include a <br /> element to force the list to wrap to the next line. By right-aligning the contents of the table cell and controlling the width of the text box and list control with the CSS width selector at runtime, you get the appearance you want (see Listing 5.8).
Listing 5.8The HTML Required to Implement the Simple ComboBox Control Shown in Figure 5.7
<table border="0" cellpadding="0" cellspacing="0"> <tr><td align="right"> <asp:TextBox id="textbox" runat="server" /><br /> <asp:ListBox id="listbox" runat="server" /> </td></tr> </table>
The HTML for a Drop-Down Combo Box
In the second mode of operation of a combo box control, the list is normally hidden and appears only when the user wants to select from it rather than type a value in the text box. The actual behavior of this kind of control varies to some extent, but you might want to implement it so that the user clicks the down button at the right end of the text box to show (drop down) the list. As with the simple combo box shown in the preceding section, selecting a value in the list places that value in the text box.
With this type of combo box, when the list is open, the down button changes to an up button that can be used to close the list again. As the user types in the text box, the first matching item in the list is selected. If the user selects a value in the list, it automatically closes, and that value is copied to the text box (see Figure 5.8).
Figure 5.8 The drop-down ComboBox control, showing a user typing in a value and opening the list, as well as the result of selecting from the list.
Obviously, the HTML required to implement this version of the control is more complex than that for the other version of the control; also, it depends on the browser's support for advanced features, such as CSS2. In particular, you need to be able to show and hide the list control, change the image that is displayed for the down and up buttons, and position the elements within some kind of container.
To create this type of control, you can use a <div> element as the container and CSS absolute positioning to fix the elements in the correct position. You can also use the CSS display selector to show and hide the list control. As with the simple ComboBox control, the handling of user interaction is carried out through client-side JavaScript to provide the best possible user experience, rather than posting back to the server with each user interaction.
The HTML used to create the drop-down control, shown in Listing 5.9, declares the enclosing <div> element with the position:relative style selector so that it acts as a positioning container. Within it are the declarations of the ASP.NET Web Forms controls that will implement the text box, the image button (an <input type="image"> element), and the list box.
Listing 5.9The HTML Required to Implement the Drop-Down ComboBox Control Shown in Figure 5.8
<div id="dropdiv" Style="position:relative" HorizontalAlign="Right" runat="server"> <asp:TextBox Style="vertical-align:middle" id="textbox2" runat="server" /><asp:ImageButton id="dropbtn" BorderWidth="0" Width="16" Height="20" Style="vertical-align:middle" ImageUrl="~/images/click-down.gif" runat="server" /><br /> <asp:ListBox Style="display:none;position:absolute;left:20;top:25" id="dropbox" runat="server" /> <asp:Image id="imageup" ImageUrl="~/images/click-up.gif" Style="display:none" runat="server" /> <asp:Image id="imagedown" ImageUrl="~/images/click-down.gif" Style="display:none" runat="server" /> </div>
The list box is absolutely positioned under the text box, shifted 20 pixels to the right. You can adjust the width of the text box and list control dynamically at runtime, using the width style selector, after you find out how wide it needs to be from the settings made by code or declarations in the hosting page.
There are also two Image controls, which contain the up and down images used by the image button. They are both declared with the display:none style selector so that they are not visible in the page. Note that you use the tilde (~) placeholder in the ImageUrl attributes to specify that the images reside in a folder named images under the current application root.
The ComboBox User Control Interface
You need to consider what kind of interface you should expose from the user control to allow the hosting page to modify the behavior and appearance of the controleither in code or through attributes added to the control declaration. Let's say you settle on exposing the properties and the single method shown in Table 5.1.
Table 5.1 The Interface for the ComboBox User Control
Property or Method |
Description |
IsDropDownCombo |
This property is a Boolean value, with a default of False. When it is True, a drop-down combo box is created. When it is False, a simple combo box is created. |
CssClass |
This property is a String value. It specifies the classname of the CSS style class to apply to the text box and list. |
DataSource |
This property is of type Object. It is a reference to a collection, DataReader instance, DataTable instance, or HashTable instance that contains the data to use with server-side data binding to fill the list. |
DataTextField |
This property is a String value. It is the name of the column or item in the data source that will be used to create the visible list of items. |
DataTextFormatString |
This property is a String value. It is a standard .NET-style format string that will be applied to the values in the DataTextField property when the list box is being filled. |
Items |
This property is a read-only ListItemCollection instance. It is a reference to the collection of ListItem objects that make up the list of values in the control. |
Rows |
This property is an Integer value with a default of 5. It specifies the number of rows to display in the list. |
SelectedIndex |
This property is an Integer value. It sets or returns the index of the item in the list that is currently selected. It returns -1 if no item is selected. |
SelectedItem |
This property is a read-only ListItem instance. It returns a reference to the ListItem object that is currently selected, or Nothing (null in C#) if no item is selected. |
SelectedValue |
This property is a String value. It sets or returns the text in the text box and selects the matching value in the list, if present. It returns an empty string if no item is selected and the text box is empty. |
Width |
This property is an Integer value, with a default of 150. It sets or returns the current width of the text box, in pixels. |
ShowMembers() |
This method returns a formatted string that contains a summary of the properties exposed by the control, ready to be inserted into an HTML page. |
Many of these properties map directly to properties of the controls contained within the user control. However, others are more complex to implement because they affect more than one of the contained controls. Notice that you can expose an interface that allows server-side data binding to be used to populate the list. Using data binding is a common and popular way to fill list controls, and the combo box in this example really has to support it to be useful in many applications.
The other issue is that you want to hide the constituent controls so that users are not tempted to read or set values in those controls directly. Instead, users must access them by using the properties you expose and leave it to the code within the user control to figure out how to read or apply the values in the appropriate way.
The property names in this example are going to be familiar to users of the standard Web Forms controls, making for a much more intuitive user experience with the ComboBox control. However, one unconventional feature is the ShowMembers method, which simply generates a listing of the properties of the control. For users who are not developing in an environment that can display the properties of controls (such as Visual Studio or WebMatrix), this can be useful. Figure 5.9 shows the string that is returned from the method, as it is displayed in a Web page.
Figure 5.9 The output of the ShowMembers method of the ComboBox control, as displayed in the sample Web page.
Exposing Style Properties
For this example, you do not expose much in the way of style properties. The CssClass property (as exposed by most ASP.NET Web Forms controls) can be used to change the appearance of the text box and list. One common technique is to expose the contained controls from a user control, allowing the hosting page to set all the standard properties of these controls, including all the style properties, such as BackColor, BorderWidth, and Font. However, that is not appropriate for this control.
You also need to maintain strict control over at least the width style selector that is applied to the text box and list, in order to maintain the position you want for these elements in the control. By applying the value of the CssClass property to the text box and list first and then setting the width selector afterward, you can override any setting that might upset the layout. You could extend this approach to other style selectors as well. Another approach would be to expose just, say, the Font property of the text box and list. However, any settings that the user requires can be made by applying the relevant value to the CssClass property of the control.
The Structure and Implementation of the ComboBox User Control
To give you a feel for the structure and implementation of the ComboBox user control, Listing 5.10 and, later in this chapter, Listing 5.11 show an outline of the content with the actual code removed for clarity. The following sections of this chapter fill in the gaps.
Listing 5.10An Outline of the Server-Side Script Section of the ComboBox User Control
<%@Control Language="VB" %> <script runat="server"> <%-------------- Private Internal Variables ------------------%> Private _width As Integer = 150 Private _rows As Integer = 5 <%------------------ Public Method ---------------------------%> Public Function ShowMembers() As String ... End Function <%-------------- Public Property Variables -------------------%> Public IsDropDownCombo As Boolean = False Public CssClass As String Public DataSource As Object Public DataTextField As String Public DataTextFormatString As String <%------------ Property Accessor Declarations ----------------%> Public Property Width As Integer ... End Property Public Property Rows As Integer ... End Property Public Property SelectedValue As String ... End Property Public ReadOnly Property Items As ListItemCollection ... End Property Public ReadOnly Property SelectedItem As ListItem ... End Property Public Property SelectedIndex As Integer ... End Property <%------------------------------------------------------------%> ... ... code to set the properties of the constituent controls ... and create the remaining output that is required ... </script>
Following the opening Control directive, which tells ASP.NET that this is a user control, is the server-side script section. It declares two Private variables that you use within the control to maintain values for the properties that are exposed. This is followed by the Public declaration of the ShowMembers method and the declaration of five Public variables. You set default values for some of the Private and Public variables, and these will be used if the page does not provide specific values at runtime.
Exposing Properties As Public Variables
One of the easiest ways to expose properties from a user control is through Public variables. The values are, of course, accessible from within the user control because they are just ordinary variables. However, the user of the control can read or set these directly, by referencing them through the id property of the user control when declared in the hosting page. For example, if the ComboBox control is declared as follows:
<ahh:ComboBox id="MyCombo" runat="server" />
the user can set the IsDropDownCombo property with this:
MyCombo.IsDropDownCombo = True
The user can also set the property declaratively in the usual .NET Web Forms control way:
<ahh:ComboBox id="MyCombo" IsDropDownCombo="True" runat="server" />
Setting Property Values Through Attributes
Note that when you're setting a property value through attributes, regardless of the data type, the value must be enclosed in single or double quotes. This is exactly the same way the standard .NET server controls work. The value is converted to the correct data type automatically when the control is compiled and instantiated by ASP.NET.
Exposing Properties by Using Accessor Routines
The six remaining properties of the ComboBox user control are declared using Public accessor routines. An accessor routine allows the declaration of a property as read-only, write-only, or read/write, and it allows you to execute code when the value is set or read (whether it is set in code in the hosting page or through an attribute in the declaration of the control). You'll see these property accessor routines soon, after you look at the remainder of the user control structure.
Outputting the Appropriate HTML
Listing 5.11 shows the remaining content of the user control. You know that you will have to generate two different chunks of HTML, depending on whether you are creating a simple combo box or the drop-down variety. To do this, you declare both versions, enclosing each one in an ASP.NET PlaceHolder control, with its Visible property set to False. At runtime, all you need to do is change the Visible property to True for the relevant PlaceHolder control, and the correct section of HTML will be output.
Listing 5.11The Visible User Interface Section of the ComboBox User Control
<%----------------- List-style Combo Box ---------------------%> <asp:PlaceHolder id="pchStandard" visible="false" runat="server"> <table border="0" cellpadding="0" cellspacing="0"> <tr><td align="right"> <asp:TextBox id="textbox" runat="server" /><br /> <asp:ListBox id="listbox" runat="server" /> </td></tr> </table> </asp:PlaceHolder> <%----------------- Drop-down Combo Box ----------------------%> <asp:PlaceHolder id="pchDropDown" visible="false" runat="server"> <div id="dropdiv" Style="position:relative" HorizontalAlign="Right" runat="server"> <asp:TextBox Style="vertical-align:middle" id="textbox2" runat="server" /><asp:ImageButton id="dropbtn" BorderWidth="0" Width="16" Height="20" Style="vertical-align:middle" ImageUrl="~/images/click-down.gif" runat="server" /><br /> <asp:ListBox Style="display:none;position:absolute;left:20;top:25" id="dropbox" runat="server" /> <asp:Image id="imageup" ImageUrl="~/images/click-up.gif" Style="display:none" runat="server" /> <asp:Image id="imagedown" ImageUrl="~/images/click-down.gif" Style="display:none" runat="server" /> </div> </asp: PlaceHolder>
The ShowMembers Method
The declaration of the ShowMembers method is almost trivial. In the Public function that implements the method, you simply construct a string that contains the required formatted HTML, and you return it as the value of the function (see Listing 5.12).
Listing 5.12The Implementation of the ShowMembers Method
Public Function ShowMembers() As String Dim sResult As String = "<b>Combo Box User Control</b>" _ & "</p><b>Properties:</b><br />" _ & "IsDropDownCombo (Boolean, default False)<br />" _ & "CssClass (String)<br />" _ & "DataSource (Object)<br />" _ & "DataTextField (String)<br />" _ & "DataTextFormatString (String)<br />" _ & "Items (ListItemCollection, Read-only)<br />" _ & "Rows (Integer, default 5)<br />" _ & "SelectedIndex (Integer)<br />" _ & "SelectedItem (ListItem, Read-only)<br />" _ & "SelectedValue (String)<br />" _ & "Width (Integer, default 150 px)" Return sResult End Function
Code in the hosting page can then display this string to the user. In the sample page, you simply use it to set the Text property of an ASP.NET Label control declared within the page:
lblResult.Text = MyCombo.ShowMembers()
Public Property Accessor Declarations
We mentioned the use of property accessor routines earlier in this chapter. This section looks at the implementation in the ComboBox user control. The simplest type of property accessor is shown in Listing 5.13. This property accessor exposes a read/write property that returns the value of an internal variable or sets the value of the internal variable to the value provided by code or in the control declaration within the hosting page. The value assigned to the property from the hosting page must be able to be cast (converted) into the correct data type, as defined in the property accessor declaration, or an exception will be raised.
Listing 5.13A Simple Property Accessor Routine
Public Property property-name As data-type Get Return internal-variable End Get Set internal-variable = value End Set End Property
In the example in Listing 5.13, the new value for the internal variable is obtained using the keyword value, which is automatically set to the value assigned to the property. An alternative approach is to specify the name of the variable that will receive the new value when the property is set, as shown in Listing 5.14.
Listing 5.14A Property Accessor Routine That Specifies the Variable TheNewValue
Public Property property-name(TheNewValue) As data-type Get Return internal-variable End Get Set _ internal-variable = TheNewValue End Set End Property
Read-Only and Write-Only Property Accessors
If you need to implement properties as read-only or write-only, you omit the Get or Set section, as appropriate. However, in Visual Basic .NET you must also add the ReadOnly or WriteOnly keyword to the property declaration, as shown in Listing 5.15.
Listing 5.15Specifying Read-Only and Write-Only Property Accessors
Public ReadOnly Property property-name As data-type Get Return internal-variable End Get End Property Public WriteOnly Property property-name As data-type Set internal-variable = value End Set End Property
Property Accessors in C#
Listing 5.16 shows how you declare a property accessor in C#. Other than the use of curly braces, the overall approach is identical to that in Visual Basic .NET, with one exception: You don't use the ReadOnly and WriteOnly keywords in C# for read-only and write-only properties.
Listing 5.16Specifying Property Accessors in C#
public data-type property-name { get { return internal-variable; } set { internal-variable = value; } }
The Property Accessors for the ComboBox User Control
The ComboBox control in this example has a property named Width that you expose via an accessor routine rather than as a Public variable. This is because you want to be able to execute some code when the property is set, which isn't possible if you just expose a variable from within the user control. When the user sets the Width property, you want to accept the value only if it is greater than 20 (it represents the width of the control, in pixels). So in the Get section of the accessor, you copy the value to the internal variable named _width only if it's greater than 20 (see Listing 5.17).
Listing 5.17The Property Accessor for the Width Property
Public Property Width As Integer Get Return _width End Get Set If value > 20 Then _width = value SetWidth() End If End Set End Property
If the value is accepted, you then have to make sure all the constituent controls that use the value are correctly updated. Listing 5.18 shows how you use the value of the internal variable _width to set the width CSS style selector for the containing <div> element, the text box, and the list. The code in Listing 5.18 checks the value of the IsDropDown property first and then sets the values for the appropriate controls; however, you could just set them all, even though some controls will not actually be output to the client.
Listing 5.18The SetWidth Routine That Applies the Width Property
Private Sub SetWidth() If IsDropDownCombo = True Then dropdiv.Style("width") = _width.ToString() textbox2.Style("width") = (_width - 17).ToString() dropbox.Style("width") = (_width - 20).ToString() Else textbox.Style("width") = _width.ToString() listbox.Style("width") = (_width - 20).ToString() End If End Sub
The same principles apply to the Rows property as to the Width property. Rows specifies the number of items that will be visible in the fixed or drop-down list of the ComboBox control. The accessor for this property accepts only values greater than zero, and it then applies the specified value to the appropriate list control (see Listing 5.19). Again, you only set the value of the appropriate control, but you could set both, even though only one will be output to the client.
Listing 5.19The Rows Property Accessor and SetRows Routine
Public Property Rows As Integer Get Return _rows End Get Set If value > 0 Then _rows = value SetRows() End If End Set End Property ... Private Sub SetRows() If IsDropDownCombo = True Then dropbox.Rows = _rows Else listbox.Rows = _rows End If End Sub
The Items property of the ComboBox control exposes the items in the fixed or drop-down list section of the ComboBox control as a ListItemCollection instance, just like all the other standard Web Forms list controls (ListBox, DropDownList, RadioButtonList, and so on). And, like the standard controls, this property is read-only. You just need to return a reference to the Items property of the appropriate ListBox control within the user control, as shown in Listing 5.20.
Listing 5.20The Read-Only Items Property Accessor
Public ReadOnly Property Items As ListItemCollection Get If IsDropDownCombo Then Return dropbox.Items Else Return listbox.Items End If End Get End Property
The SelectedItem, SelectedIndex, and SelectedValue Properties
The three remaining properties exposed by the ComboBox controlSelectedItem, SelectedIndex, and SelectedValueprovide information about the item that is currently selected. Again, following the model of the standard list controls, you expose a read-only property named SelectedItem that returns a ListItem instance representing the first selected item within the Items collection and a read/write property named SelectedIndex that sets or returns the index of the first selected item. You also provide a read/write property named SelectedValue. This property was added to the ASP.NET Web Forms ListControl base class (from which all the list controls are descended) in version 1.1 of the .NET Framework.
Listing 5.21 shows the implementation of the SelectedItem property. This isn't quite as straightforward as the properties examined so far. If the user has selected an item in the list, it will also be in the text box. However, the user may have typed into the text box a value that is not in the list (and so no item will be selected in the list). So the value in the text box really represents the selected value of the control.
Therefore, depending on which mode the control is in and whether there is a value selected in the appropriate list, you create a new ListItem instance or return a reference to an existing one. Notice that when you are creating a new ListItem control instance for the text box, you set both the Text and Value properties to the value of the text box.
Listing 5.21The SelectedItem Property Accessor Routine
Public ReadOnly Property SelectedItem As ListItem Get If IsDropDownCombo Then If dropbox.SelectedIndex < 0 Then Return New ListItem(textbox2.Text, textbox2.Text) Else Return dropbox.SelectedItem End If Else If listbox.SelectedIndex < 0 Then Return New ListItem(textbox.Text, textbox.Text) Else Return listbox.SelectedItem End If End If End Get End Property
The SelectedIndex property is a little more complex than the other properties. It's a read/write property; however, the Get section is simple enoughyou just return the value of the SelectedIndex property for the appropriate list (see Listing 5.22). The complexity in the Set section comes from the fact that you first have to ensure that the new value is within the bounds of the list. It can be -1 to deselect any existing selected value, or it can be between zero and one less than the length of the ListItemCollection instance. If the new value is valid, you can set the SelectedIndex property of the list control and then copy that value into the text box as well (as would happen if the user selected that value in the browser).
Listing 5.22The SelectedIndex Property Accessor Routine
Public Property SelectedIndex As Integer Get If IsDropDownCombo Then Return dropbox.SelectedIndex Else Return listbox.SelectedIndex End If End Get Set If IsDropDownCombo Then If (value >= -1) And (value < dropbox.Items.Count) Then dropbox.SelectedIndex = value textbox2.Text = dropbox.Items(SelectedIndex).Text End If Else If (value >= -1) And (value < listbox.Items.Count) Then listbox.SelectedIndex = value textbox.Text = listbox.Items(SelectedIndex).Text End If End If End Set End Property
Finally, the most complex of all the property accessors is SelectedValue. As shown in Listing 5.23, you can get the selected value from the appropriate list within the user control easily enough (depending on the mode the ComboBox control is in). However, setting the SelectedValue property involves first copying the new value to the text box and then searching through the list to see if it contains an entry with this value. If it does, you must select this item as well (or, if the value appears more than once in the list, you must select the first instance). Moreover, you have to do all this with the appropriate text box and list control, depending on the mode that the ComboBox control is currently in.
Listing 5.23The SelectedValue Property Accessor Routine
Public Property SelectedValue As String Get If IsDropDownCombo Then Return textbox2.Text Else Return textbox.Text End If End Get Set If IsDropDownCombo Then textbox2.Text = value dropbox.SelectedIndex = -1 For Each oItem As ListItem In dropbox.Items If value.Length <= oItem.Text.Length Then If String.Compare(oItem.Text.Substring(0, value.Length), _ value, True) = 0 Then oItem.Selected = True Exit For End If End If Next Else textbox.Text = value listbox.SelectedIndex = -1 For Each oItem As ListItem In listbox.Items If value.Length <= oItem.Text.Length Then If String.Compare(oItem.Text.Substring(0, value.Length), _ value, True) = 0 Then oItem.Selected = True Exit For End If End If Next End If End Set End Property
Factoring the Code in the Property Accessors
You could, of course, create routines that remove some of the repeated code shown in Listing 5.23, but the intention here is to illustrate how setting a property of a composite control (that is, a control that contains other controls) can actually involve often quite complex internal processing.
The Page_Load Event Handler for the ComboBox Control
Now that you've looked in some detail at how to expose properties from a user control, the next stage is to see what happens when the control is instantiated in a hosting page. Although many events occur during the process of loading and executing an ASP.NET page and any user controls it contains, at this point you're most interested in the Page_Load event.
The Ordering of Load and Init Events for a User Control
The Page_Load event for a user control occurs immediately after the Page_Load event for the hosting page. However, this is not the case for all events. The other useful event, Page_Init, occurs for all instances of a user control immediately before the Page_Init event of the hosting page.
You need to accomplish the following tasks during the Page_Load event of the control. They don't have to be performed in this specific order, though this is the ordering used in the example code:
Output the client-side script functions that are required to make the control work interactively.
Set the CSS selectors and CSS class for the constituent controls.
Attach the client-side event handlers to the constituent controls.
Set the server-side data-binding properties and bind the list.
Make sure that the width of the constituent controls and the number of rows in the list control are correctly set to override any conflicting CSS style property settings made in the hosting page.
Generating the Client-Side Script Section
Listing 5.24 shows the client-side script section that you must create and send to the client to enable the control to operate interactively. The selectList function runs when the user makes a selection in the list. It copies the selected value into the text box and, if the current mode is a drop-down combo box, it closes the list by calling the openList function that is shown at the end of Listing 5.24.
Listing 5.24The Client-Side Script Required for the Control
<script language='javascript'> function selectList(sCtrlID, sListID, sTextID) { var list = document.getElementById(sCtrlID + sListID); var text = document.getElementById(sCtrlID + sTextID); text.value = list.options[list.selectedIndex].text; if (sListID == 'dropbox') openList(sCtrlID); } function scrollList(sCtrlID, sListID, sTextID) { var list = document.getElementById(sCtrlID + sListID); var text = document.getElementById(sCtrlID + sTextID); var search = new String(text.value).toLowerCase(); list.selectedIndex = -1; var items = list.options; var option = new String(); for (i = 0; i < items.length; i++) { option = items[i].text.toLowerCase(); if (option.substring(0, search.length) == search ) { list.selectedIndex = i; break; } } } function openList(sCtrlID) { var list = document.getElementById(sCtrlID + 'dropbox'); var btnimg = document.getElementById(sCtrlID + 'dropbtn'); if(list.style.display == 'none') { list.style.display = 'block'; btnimg.src = document.getElementById(sCtrlID + 'imageup').src; } else { list.style.display = 'none'; btnimg.src = document.getElementById(sCtrlID + 'imagedown').src; } return false; } </script>
The scrollList function runs after the user presses and releases any key while the text box has the focus. It just has to search the list for the first matching value and select it. Notice that it ignores the letter case of the values by converting both values to lowercase before checking for a match.
The openList function runs when the user clicks the image button at the end of the text box, when the current mode is a drop-down combo box (this control is not generated for a simple combo box). It is also called, as you saw earlier, from the selectList function. The code in the openList function shows or hides the list control by switching the CSS display selector value between "block" and "none", depending on the current value, and it also swaps the src attribute of the image button to show the appropriate up or down button image.
More on Using Client-Side Script Code
Chapter 7, "Design Issues for User Controls," looks in more detail at the techniques used in this client-side code. Chapter 7 talks about client-side scripting in general and how you can integrate it with ASP.NET and your own custom controls. It also discusses browser compatibility issues. In subsequent chapters you'll see how you can build controls that adapt their behavior to different browsers.
Registering Client Script Blocks
The traditional way to generate client-side script sections in a Web page when using ASP is to simply write the code directly within the source of the page. This works fine in ASP.NET, too, because the <script> element does not contain the runat="server" attribute, so ASP.NET ignores it and sends it to the client as literal output.
What About the runat="client" Attribute?
Interestingly, the W3C specifications suggest that you use <script runat="client">, although "client" is the default value for this attribute in the browser if the value is omitted. Unfortunately, ASP.NET doesn't allow you to include this attribute, and if you try to use it, you get the error "The Runat attribute must have the value Server." This is a shame because "client" would make it more obvious what the script section was intended for.
You'll be generating the script section from within a user control, and user controls are intended to allow multiple copies to be placed in the same hosting page. In this case, you'd end up with multiple copies of the script section as well. To prevent this, you use the features of ASP.NET that are designed to inject items such as client-side script into the output generated by the page.
You first create the entire script section in a String variable, and then you register that script block with the hosting page by using the RegisterClientScriptBlock method. The string is injected into the page immediately after the opening server-side <form> tag (and after any hidden controls that ASP.NET requires, such as the one that stores the viewstate). The page also keeps track of registrations based on a string value you provide for the key. The hosting page is referenced through the Page property of the user control:
Page.RegisterClientScriptBlock("identifier", script-string)
Then, to ensure that you only ever insert one copy of the script, you can use the IsClientScriptBlockRegistered method to check whether a script section with the same identifier has already been registered. You register and insert the script section only if it hasn't been injected:
If Not Page.IsClientScriptBlockRegistered("identifier") Then Page.RegisterClientScriptBlock("identifier", script-string) End If
The Parameters for the Client-Side Functions
If you are using multiple copies of the same user control in a page, you have to make sure that the client-side script can identify which instance it should be processing. One easy way around this is to use the JavaScript keyword this, which returns a reference to the current object or control.
However, the user control in this example contains constituent controls, and these vary depending on the mode of the control. So you have to pass in several values to allow the code to process the correct constituent controls. You can see in the earlier listings that the two main client-side script functions take three parameters: the id property of the current user control and the IDs of the ListBox and TextBox controls within the current user control:
function scrollList(sCtrlID, sListID, sTextID) { ...
When a user control is inserted into a hosting page, it is usually allocated an id value within the declaration:
<ahh:ComboBox id="cboTest1" runat="server" />
If the user does not specify an id value, ASP.NET adds an autogenerated one, such as _ctl5. Either way, you can retrieve this id value from within the user control through the UniqueID property that is exposed by all controls (inherited from System.Web.UI.Control). Although the autogenerated value is often the same as the id property, it may not be if the control is used within the template of another controlfor example, in a data-bound Repeater or DataList control.
The constituent controls within a user control also have their id values massaged by ASP.NET. This is required; otherwise, multiple copies of a user control inserted into a hosting page would generate the same id values for their constituent controls. ASP.NET automatically prefixes the constituent controls with the ID of the user control itself plus an underscore. So, for example, the control with the ID value "textbox2" would appear in the control hierarchy of the hosting page with the id value "cboTest1_textbox2".
Discovering the id Values of the Controls
You can view the source of the page in the browser (by selecting View, Source in Internet Explorer) to see the id values that are generated. This is also a good way to debug your pages and find errors, as you get to see what output the user control is actually sending to the client.
Therefore, in the user control in this example, you can create the ID prefix that will be added to the ID of the constituent controls by referencing the UniqueID property of the user control (the current object, as obtained using the keyword Me in Visual Basic .NET or this in C#):
Dim sCID As String = Me.UniqueID & "_"
The Code in the Page_Load Event Handler
In the Page_Load event, you can now generate the identifier for the current control and build the client-side script as a string. You must remember to include a carriage return at the end of each line of the script and use single quotes in the code itself so that each line of code can be wrapped in double quotes. In Visual Basic .NET, you can use the built-in vbCrlf constant to output a carriage return. In C#, you just have to include \n at the end of each line and also remember to replace any forward slashes in the code with \\.
An abbreviated section of the code to create the script section is shown in Listing 5.25 (the complete code, as seen in the browser, is shown in Listing 5.24, so there is no point in repeating it all here). You register this script to inject a copy if it doesn't already exist.
Listing 5.25The First Part of the Code in the Page_Load Event Handler
Sub Page_Load() Dim sCID As String = Me.UniqueID & "_" Dim sScript As String = vbCrlf _ & "<script language='javascript'>" & vbCrlf _ & "function selectList(sCtrlID, sListID, sTextID) {" & vbCrlf _ & " var list = document.getElementById(sCtrlID + sListID);" _ & vbCrlf _ ... etc ... & "}" & vbCrlf _ & "<" & "/script>" & vbCrlf If Not Page.IsClientScriptBlockRegistered("AHHComboBox") Then Page.RegisterClientScriptBlock("AHHComboBox", sScript) End If ...
Hiding the Closing </script> Tag
You can hide the closing </script> tag from the compiler by splitting it into two sections in the source code. This is a throwback to a technique used when writing script dynamically into the page, which prevents the browser from raising an error. It isn't actually required here, but it does no harm.
The next task in the Page_Load event handler is to set the properties and attributes of the constituent text box, list, and image button controls. Listing 5.26 shows this final section of code, continuing from Listing 5.25. Recall from earlier in this chapter that the two sets of HTML declarations for the two different modes that the ComboBox control can exhibit are enclosed in PlaceHolder controls that have their Visible property set to False. So depending on the mode you're currently in, you make the appropriate section of HTML visible by setting the Visible property of the PlaceHolder control that encloses it to True.
Listing 5.26The Remaining Code for the Page_Load Event Handler
... If IsDropDownCombo = True Then pchDropDown.Visible = True If CssClass <> "" Then dropbox.CssClass = CssClass.ToString() textbox2.CssClass = CssClass.ToString() End If dropbox.Attributes.Add("onclick", "selectList('" & sCID _ & "', 'dropbox', 'textbox2')") textbox2.Attributes.Add("onkeyup", "scrollList('" & sCID _ & "', 'dropbox', 'textbox2')") dropbtn.Attributes.Add("onclick", "return openList('" _ & sCID & "')") dropbox.DataSource = DataSource dropbox.DataTextField = DataTextField dropbox.DataTextFormatString = DataTextFormatString dropbox.DataBind() Else pchStandard.Visible = True If CssClass <> "" Then listbox.CssClass = CssClass textbox.CssClass = CssClass End If listbox.Attributes.Add("onclick", "selectList('" & sCID _ & "', 'listbox', 'textbox')") textbox.Attributes.Add("onkeyup", "scrollList('" & sCID _ & "', 'listbox', 'textbox')") listbox.DataSource = DataSource listbox.DataTextField = DataTextField listbox.DataTextFormatString = DataTextFormatString listbox.DataBind() End If SetWidth() SetRows() End Sub
Now you can apply any CSS classname that may have been specified for the CssClass property to both the list and text box. Then you add the attributes to the text box and list that attach the client-side script functions. You use the complete ID of this instance of the user control (which you generated earlier) and specify the appropriate text box and list control IDs. If you're creating a drop-down combo box, you also have to connect the openList function to the list control.
Next, you set the data binding properties of the list within the user control to the values specified for the matching properties of the user control and call the DataBind method. In fact, you could check whether the DataSource property has been set first, before setting the properties and calling DataBind. This would probably be marginally more efficient, although the DataBind method does nothing if the DatSource property is empty.
Finally, you call the SetWidth and SetRows routines again to ensure that any conflicting CSS styles are removed from the constituent controls. And that's it; the ComboBox control is complete and ready to go. You'll use it in a couple simple sample pages next to demonstrate setting the properties and using data binding.