As I promised in the previous post, I will detail how to implement the HTML <optgroup> tag inside a <select> tag in this post. The HTML markup looks as follows when using the optgroup tag:
1: <select>
2: <optgroup label="Group 1">
3: <option>Item 1</option>
4: <option>Item 2</option>
5: <option>Item 3</option>
6: </optgroup>
7: <optgroup label="Group 2">
8: <option>Item 4</option>
9: <option>Item 5</option>
10: <option>Item 6</option>
11: <option>Item 7</option>
12: </optgroup>
That code would render something like the following:
As you can see, the optgroup tags render as bold and italicized group labels (that cannot be selected), and the items under each are indented. Note that in IE the optgroup tags cannot be styled but FireFox does permit styling.
As we know, the select tag is implemented in .net as the DropDownList control. At render time, the control renders <option> tags from its Items collection. At first glance, it would seem like all one has to do to subclass the control and add an OptionGroups collection property which is a collection of ListItems (the type of the standard Items property). While this is part of what needs to be done, it is only the beginning.
Adding Option Groups
To start off, obviously we must create a class that inherits from DropDownList. I called this control OptionGroupDropDownList. Next, we need to add something to hold the optgroup tags. I added an OptionGroups property to the control:
1: public OptionGroupCollection OptionGroups
2: {
3: get
4: {
5: if (_optionGroups == null)
6: {
7: _optionGroups = new OptionGroupCollection();
8: if (_isTrackingViewState)
9: _optionGroups.TrackViewState();
10: }
11:
12: return _optionGroups;
13: }
14: }
As you can see, the OptionGroups property is of type OptionGroupCollection. Two helper classes are required to implement this control: the aforementioned OptionGroupCollection and OptionGroup. Additionally, some state management things are going on in the property get accessor (there's no need for a set accessor). The state management is the thing that complicates this control beyond a simple subclass and render override that the UnEncodedDropDownList was. You'll see why we need to manage state in a moment.
OptionGroupCollection And OptionGroup Classes
To make things easy, OptionGroupCollection is simply a wrapper around a List<OptionGroup> private member. The OptionGroup is a couple of properties necessary to implement the optgroup tag and a wrapper for a ListItemCollection, which is a collection of the ListItems (option tags) that will render inside the optgroup tag. This is where the state management comes in. To make it easy for the user I wanted the items inside each OptionGroup to look and behave the same as the items in a normal DropDownList so I used the same class the built-in control uses: ListItemCollection. However, because ListItemCollection (and the ListItem child objects) are sealed, it is somewhat more cumbersome to manage the state of items in this collection than it would be otherwise. Luckily, ListItemCollection implements the IStateManager interface, which we will use to tell it when to manage its own state and obtain a state object.
State Management
To correctly manage state, the OptionGroupDropDownList control must manually manage its own state by overriding all of the IStateManager methods of the base DropDownList control, most important among them being TrackViewState:
1: protected override void TrackViewState()
2: {
3: base.TrackViewState();
4: this.OptionGroups.TrackViewState();
5: _isTrackingViewState = true;
6: }
TrackingViewState signals the control (and any child controls that also need to know via manual forwarding) to start tracking viewstate. Without this signal, the ListItemCollection would never save its state between server round-trips. OptionGroupCollection and OptionGroup both implement IStateManager and therefore TrackViewState.
Two other important methods to override in the OptionGroupDropDownList (with regard to state management) are SaveViewState and LoadViewState. This is because the OptionGroupsCollection's state needs to be saved and loaded (which, in turn, saves the OptionGroup's, which in turn saves the ListItemCollection's state, which in turn saves the individual ListItem's state - it's just a chain and each individual object is responsible for saving its own state as an object and returning that object in SaveViewState). This is what these two method overrides look like:
1: protected override object SaveViewState()
2: {
3: return new Pair(base.SaveViewState(), _optionGroups.SaveViewState());
4: }
5:
6: protected override void LoadViewState(object savedState)
7: {
8: Pair viewState = (Pair)savedState;
9:
10: base.LoadViewState(viewState.First);
11: this.OptionGroups.LoadViewState(viewState.Second);
12: }
Rendering
The last important method to override is the RenderContents method which actually renders out our new OptionGroup items. This method is not very complicated and simply iterates the OptionGroups and OptionGroup.Items:
1: protected override void RenderContents(System.Web.UI.HtmlTextWriter writer)
2: {
3: if (this.OptionGroups.Count > 0)
4: foreach (OptionGroup optionGroup in this.OptionGroups)
5: {
6: writer.AddAttribute("label", optionGroup.LabelText);
7: optionGroup.LabelStyle.AddAttributesToRender(writer);
8: writer.RenderBeginTag("optgroup");
9:
10: foreach (ListItem item in optionGroup.Items)
11: {
12: if (item.Enabled)
13: {
14: if (item.Selected)
15: writer.AddAttribute(HtmlTextWriterAttribute.Selected, "selected");
16:
17: writer.AddAttribute(HtmlTextWriterAttribute.Value, item.Value);
18:
19: if (item.Attributes != null && item.Attributes.Count > 0)
20: item.Attributes.Render(writer);
21:
22: if (this.Page != null)
23: this.Page.ClientScript.RegisterForEventValidation(this.UniqueID, item.Value);
24:
25: writer.RenderBeginTag("option");
26: writer.Write(item.Text);
27: writer.RenderEndTag();
28: }
29: }
30:
31: writer.RenderEndTag();
32: }
33: else
34: base.RenderContents(writer);
35: }
Use
To use the control simply add OptionGroups and items to the OptionGroups you create:
1: OptionGroupDropDownList ogdMain = new OptionGroupDropDownList();
2:
3: OptionGroup group = ogdMain.OptionGroups.Add("Group 1");
4: group.Items.Add("Item 1");
5: group.Items.Add("Item 2");
6: group.Items.Add("Item 3");
7:
8: group = ogdMain.OptionGroups.Add("Group 2");
9: group.Items.Add("Item 4");
10: group.Items.Add("Item 5");
11: group.Items.Add("Item 6");
12: group.Items.Add("Item 7");
13:
14: Page.Controls.Add(ogdMain);
Of course, you can declare the control on the aspx page; I have created it here programmatically just so the code runs if you copy and paste.
This control works just fine inside an AJAX UpdatePanel, in case you're wondering. There are some other incidentals with dealing with post data, which you can check out in the code, linked below.
Conclusion/Caveats
This was an interesting control to implement as I had to do a bunch of spelunking using Reflector to figure out how ListItemCollection saves state. It permits me to use the optgroup tag in a select tag, which is not exactly a requirement very often, but it's one of those things that when you need it, you need it.
Of course there are caveats: The default Items collection is not rendered in any way. I didn't need both at the same time so I didn't implement it. You can, however, change the code however you like. You'll want to mess about in the RenderContents method. Additionally, not a whole lot of testing has been done. I know, as I stated above, that this control works with an AJAX postback just perfectly, however, I'm not exactly sure how it works with regular postbacks. Most likely it works fine since the AJAX scenario is more complicated, but I make no guarantees.
OptionGroupDropDownList.cs (7.29 kb)
Currently rated 4.0 by 1 people
- Currently 4/5 Stars.
- 1
- 2
- 3
- 4
- 5