Home > Articles > Programming > ASP .NET

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

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 7Figure 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.8—The 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 8Figure 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.9—The 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 control—either 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 9Figure 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.10—An 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.11—The 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.12—The 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.13—A 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.14—A 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.15—Specifying 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.16—Specifying 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.17—The 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.18—The 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.19—The 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.20—The 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 control—SelectedItem, SelectedIndex, and SelectedValue—provide 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.21—The 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 enough—you 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.22—The 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.23—The 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.24—The 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 control—for 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.25—The 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.26—The 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.

  • + Share This
  • 🔖 Save To Your Account