ASP.NET DropDownList With Groups

A long time ago I submitted a request to the ASP.NET team for having the standard DropDownList support HTML’s optgroup tag: http://aspnet.codeplex.com/workitem/10318. For those of you not familiar with this tag – that has been around for quite some time, by the way –, it allows for something like this:

In a nutshell, we can have grouped list items. The issue is still open, but, since it is such a handy feature, and one that I need quite often, I decided to implement support for it on top of the existing DropDownList. Again, my solution uses tag mapping, which you should know from my previous posts, so I can add support for it on all of the already declared DropDownLists.

I wanted to support the two basic scenarios:

  1. Adding ListItem entries on markup or by code;
  2. Having the ListItem entries created dynamically through data binding.

I started by implementing a control that inherits from DropDownList:

   1: public class GroupedDropDownList : DropDownList
   2: {
   3:     public String DataGroupField
   4:     {
   5:         get;
   6:         set;
   7:     }
   8:  
   9:     protected override void PerformDataBinding(IEnumerable dataSource)
  10:     {
  11:         base.PerformDataBinding(dataSource);
  12:  
  13:         if ((String.IsNullOrWhiteSpace(this.DataGroupField) == false) && (dataSource != null))
  14:         {
  15:             ListItemCollection items = this.Items;
  16:             IEnumerable<Object> data = dataSource.OfType<Object>();
  17:             Int32 count = data.Count();
  18:  
  19:             for (Int32 i = 0; i < count; ++i)
  20:             {
  21:                 String group = DataBinder.Eval(data.ElementAt(i), this.DataGroupField) as String ?? String.Empty;
  22:  
  23:                 if (String.IsNullOrWhiteSpace(group) == false)
  24:                 {
  25:                     items[i].Attributes["Group"] = group;
  26:                 }
  27:             }
  28:         }
  29:     }
  30:  
  31:     protected override void RenderContents(HtmlTextWriter writer)
  32:     {
  33:         ListItemCollection items = this.Items;
  34:         Int32 count = items.Count;
  35:         var groupedItems = items.OfType<ListItem>().GroupBy(x => x.Attributes["Group"] ?? String.Empty).Select(x => new { Group = x.Key, Items = x.ToList() });
  36:  
  37:         if (count > 0)
  38:         {
  39:             Boolean flag = false;
  40:  
  41:             foreach (var groupedItem in groupedItems)
  42:             {
  43:                 if (String.IsNullOrWhiteSpace(groupedItem.Group) == false)
  44:                 {
  45:                     writer.WriteBeginTag("optgroup");
  46:                     writer.WriteAttribute("label", groupedItem.Group);
  47:                     writer.Write('>');
  48:                 }
  49:  
  50:                 for (Int32 i = 0; i < groupedItem.Items.Count; ++i)
  51:                 {
  52:                     ListItem item = groupedItem.Items[i];
  53:  
  54:                     if (item.Enabled == true)
  55:                     {
  56:                         writer.WriteBeginTag("option");
  57:  
  58:                         if (item.Selected == true)
  59:                         {
  60:                             if (flag == true)
  61:                             {
  62:                                 this.VerifyMultiSelect();
  63:                             }
  64:  
  65:                             flag = true;
  66:  
  67:                             writer.WriteAttribute("selected", "selected");
  68:                         }
  69:  
  70:                         writer.WriteAttribute("value", item.Value, true);
  71:  
  72:                         if (item.Attributes.Count != 0)
  73:                         {
  74:                             item.Attributes.Render(writer);
  75:                         }
  76:  
  77:                         if (this.Page != null)
  78:                         {
  79:                             this.Page.ClientScript.RegisterForEventValidation(this.UniqueID, item.Value);
  80:                         }
  81:  
  82:                         writer.Write('>');
  83:                         HttpUtility.HtmlEncode(item.Text, writer);
  84:                         writer.WriteEndTag("option");
  85:                         writer.WriteLine();
  86:                     }
  87:                 }
  88:  
  89:                 if (String.IsNullOrWhiteSpace(groupedItem.Group) == false)
  90:                 {
  91:                     writer.WriteEndTag("optgroup");
  92:                 }
  93:             }
  94:         }
  95:     }
  96: }

In case you are wondering, the implementation of RenderContents comes from the standard ListControl, here: http://msdn.microsoft.com/en-us/library/4c9zdz95.aspx. As you can see, I overrode two methods and added an extra property:

  • RenderContents is the method responsible for producing the HTML for each of the ListItem items in the Items collection; I modified it so as to output an optgroup declaration for each of the ListItems that contain references to a group, keeping the order;
  • PerformDataBinding is where the data binding process actually populates the  Items collection; I changed it so as to include a Group attribute, in case the DataGroupField property is set.

The new DataGroupField property, similar to DataTextField and DataValueField, if specified, will contain the name of the property in the data source that holds each item’s group name, when using data binding. If not specified, no grouping will occur. Do note that this only applies to data binding, not manually added ListItems.

Now, for using this control, we can either use it explicitly:

   1: <My:GroupedDropDownList runat="server">
   2:     <asp:ListItem Group="Web" Text="CSS"/>
   3:     <asp:ListItem Group="Web" Text="HTML"/>
   4:     <asp:ListItem Group="Web" Text="JavaScript"/>
   5:     <asp:ListItem Group="Windows" Text="WPF"/>
   6:     <asp:ListItem Group="Windows" Text="Windows Forms"/>
   7: </My:GroupedDropDownList>

Or use a tag mapping:

   1: <tagMapping>
   2:     <add tagType="System.Web.UI.WebControls.DropDownList" mappedTagType="MyNamespace.GroupedDropDownList, MyAssembly"/>
   3: </tagMapping>

In which case, you keep the standard DropDownList declaration but add the Group attribute to the ListItems, which works because ListItem implements IAttributeAccessor:

   1: <asp:DropDownList runat="server">
   2:     <asp:ListItem Group="Web" Text="CSS"/>
   3:     <asp:ListItem Group="Web" Text="HTML"/>
   4:     <asp:ListItem Group="Web" Text="JavaScript"/>
   5:     <asp:ListItem Group="Windows" Text="WPF"/>
   6:     <asp:ListItem Group="Windows" Text="Windows Forms"/>
   7: </asp:DropDownList>

And if you prefer to use data binding, you add an extra attribute for the DataGroupField property (DropDownList also implements IAttributeAccessor):

   1: <asp:DropDownList runat="server" ID="list" DataTextField="Name" DataValueField="Id" DataGroupField="Type"/>

And on code behind (or a data source control):

   1: protected void OnLoad(EventArgs e)
   2: {
   3:     var items = new List<Object>();
   4:     items.Add(new { Text = "CSS", Group = "Web" });
   5:     items.Add(new { Text = "HTML", Group = "Web" });
   6:     items.Add(new { Text = "JavaScript", Group = "Web" });
   7:     items.Add(new { Text = "WPF", Group = "Windows" });
   8:     items.Add(new { Text = "Windows Forms", Group = "Windows" });
   9:  
  10:     this.list.DataSource = items;
  11:     this.list.DataBind();
  12:  
  13:     base.OnLoad(e);
  14: }

And you will get all items nicely grouped!

As you can see, tag mapping is very powerful, and, if used with controls that support extensibility, can be very useful. Hope this helps!

                             

5 Comments

Comments have been disabled for this content.