Grouping List Items in ASP.Net DropDownList
As many of us know,currently ASP.Net dropdownlist control doesn’t support grouping list items,though you can create it easily in pure html using "select" :
< select id = "myselect" > < optgroup label = "COUNTRY" > < option label = "country1" >country1</ option > < option label = "country2" >country2</ option > </ optgroup > < optgroup label = "STATE" > < option label = "STATE1" >STATE1</ option > < option label = "STATE2" >STATE2</ option > </ optgroup > </ select > |
Which will be rendered as:
as you can see it's easily done using “select” tag along with optgroup,you can refer to W3C site for more information on this tag.
In order to add the grouping support to ASP.Net dropdownlist we have to customize it by coding
How to do it in ASP.NET DropDownList:
Asp.net dropdownlist doesn't support grouping by default,we have to override it’s functionality/rending. Here are the steps:
- Create a new project of type ClassLibrary and name it “GroupDropDownList”
- Remove any class files added by the project then create a new class file named “GroupDropDownList.cs”
- Add reference to “System.Web“
- Replace GroupDropDownList.cs code with this:
using System; using System.ComponentModel; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Collections; namespace GroupDropDownList { /// <summary> /// Summary description for GroupDropDownList. /// </summary> [ToolboxData( "<{0}:GroupDropDownList runat=server></{0}:GroupDropDownList>" )] public class GroupDropDownList : DropDownList { /// <summary> /// The field in the datasource which provides values for groups /// </summary> [DefaultValue( "" ), Category( "Data" )] public virtual string DataGroupField { get { object obj = ViewState[ "DataGroupField" ]; if (obj != null ) { return ( string )obj; } return string .Empty; } set { ViewState[ "DataGroupField" ] = value; } } /// <summary> /// if a group doesn't has any enabled items,there is no need /// to render the group too /// </summary> /// <param name="groupName"></param> /// <returns></returns> private bool IsGroupHasEnabledItems( string groupName) { ListItemCollection items = Items; for ( int i = 0; i < items.Count; i++) { ListItem item = items[i]; if (item.Attributes[ "DataGroupField" ].Equals(groupName) && item.Enabled) { return true ; } } return false ; } /// <summary> /// Render this control to the output parameter specified. /// Based on the source code of the original DropDownList method /// </summary> /// <param name="writer"> The HTML writer to write out to </param> protected override void RenderContents(HtmlTextWriter writer) { ListItemCollection items = Items; int itemCount = Items.Count; string curGroup = String.Empty; bool bSelected = false ; if (itemCount <= 0) { return ; } for ( int i = 0; i < itemCount; i++) { ListItem item = items[i]; string itemGroup = item.Attributes[ "DataGroupField" ]; if (itemGroup != null && itemGroup != curGroup && IsGroupHasEnabledItems(itemGroup)) { if (curGroup != String.Empty) { writer.WriteEndTag( "optgroup" ); writer.WriteLine(); } curGroup = itemGroup; writer.WriteBeginTag( "optgroup" ); writer.WriteAttribute( "label" , curGroup, true ); writer.Write( '>' ); writer.WriteLine(); } // we don't want to render disabled items if (item.Enabled) { writer.WriteBeginTag( "option" ); if (item.Selected) { if (bSelected) { throw new HttpException( "Cant_Multiselect_In_DropDownList" ); } bSelected = true ; writer.WriteAttribute( "selected" , "selected" , false ); } writer.WriteAttribute( "value" , item.Value, true ); writer.Write( '>' ); HttpUtility.HtmlEncode(item.Text, writer); writer.WriteEndTag( "option" ); writer.WriteLine(); } } if (curGroup != String.Empty) { writer.WriteEndTag( "optgroup" ); writer.WriteLine(); } } /// <summary> /// Perform data binding logic that is associated with the control /// </summary> /// <param name="e">An EventArgs object that contains the event data</param> protected override void OnDataBinding(EventArgs e) { // Call base method to bind data base .OnDataBinding(e); if (DataGroupField == String.Empty) { return ; } // For each Item add the attribute "DataGroupField" with value from the datasource IEnumerable dataSource = GetResolvedDataSource(DataSource, DataMember); if (dataSource != null ) { ListItemCollection items = Items; int i = 0; string groupField = DataGroupField; foreach ( object obj in dataSource) { string groupFieldValue = DataBinder.GetPropertyValue(obj, groupField, null ); ListItem item = items[i]; item.Attributes.Add( "DataGroupField" , groupFieldValue); i++; } } } /// <summary> /// This is copy of the internal ListControl method /// </summary> /// <param name="dataSource"></param> /// <param name="dataMember"></param> /// <returns></returns> private IEnumerable GetResolvedDataSource( object dataSource, string dataMember) { if (dataSource != null ) { var source1 = dataSource as IListSource; if (source1 != null ) { IList list1 = source1.GetList(); if (!source1.ContainsListCollection) { return list1; } var list = list1 as ITypedList; if (list != null ) { var list2 = list; PropertyDescriptorCollection collection1 = list2.GetItemProperties( new PropertyDescriptor[0]); if ((collection1 == null ) || (collection1.Count == 0)) { throw new HttpException( "ListSource_Without_DataMembers" ); } PropertyDescriptor descriptor1 = collection1[0]; if (! string .IsNullOrWhiteSpace(dataMember)) { descriptor1 = collection1.Find(dataMember, true ); } if (descriptor1 != null ) { object obj1 = list1[0]; object obj2 = descriptor1.GetValue(obj1); var enumerable = obj2 as IEnumerable; if (enumerable != null ) { return enumerable; } } throw new HttpException( "ListSource_Missing_DataMember" ); } } var source = dataSource as IEnumerable; if (source != null ) { return source; } } return null ; } #region Internal behaviour /// <summary> /// Saves the state of the view. /// </summary> protected override object SaveViewState() { // Create an object array with one element for the CheckBoxList's // ViewState contents, and one element for each ListItem in skmCheckBoxList var state = new object [Items.Count + 1]; object baseState = base .SaveViewState(); state[0] = baseState; // Now, see if we even need to save the view state bool itemHasAttributes = false ; for ( int i = 0; i < Items.Count; i++) { if (Items[i].Attributes.Count == 0) continue ; itemHasAttributes = true ; // Create an array of the item's Attribute's keys and values var attribKv = new object [Items[i].Attributes.Count * 2]; int k = 0; foreach ( string key in Items[i].Attributes.Keys) { attribKv[k++] = key; attribKv[k++] = Items[i].Attributes[key]; } state[i + 1] = attribKv; } // return either baseState or state, depending on if any ListItems had attributes return itemHasAttributes ? state : baseState; } /// <summary> /// Loads the state of the view. /// </summary> /// <param name="savedState">State of the saved.</param> protected override void LoadViewState( object savedState) { if (savedState == null ) return ; // see if savedState is an object or object array var objects = savedState as object []; if (objects != null ) { // we have an array of items with attributes object [] state = objects; base .LoadViewState(state[0]); // load the base state for ( int i = 1; i < state.Length; i++) { if (state[i] != null ) { // Load back in the attributes var attribKv = ( object []) state[i]; for ( int k = 0; k < attribKv.Length; k += 2) Items[i - 1].Attributes.Add(attribKv[k].ToString(), attribKv[k + 1].ToString()); } } } else { // we have just the base state base .LoadViewState(savedState); } } #endregion } } |
- Open your website project then add a reference to “GroupDropDownList.dll”
- In your aspx page add a register directive:
<%@ Register TagPrefix="customControl" Namespace="GroupDropDownList" Assembly="GroupDropDownList" %> |
- Now add your control like this:
<
customControl:GroupDropDownList
ID
=
"ddlUsers"
runat
=
"server"
></
customControl:GroupDropDownList
>
How to bind the control:
let's assume our data looks like this:
Id |
Username | UserType |
1 | John | Admins |
2 | Joey | Admins |
3 | Majid | Users |
4 | Sam | Users |
Before you bind the data to your control make sure your data is sorted by DataGroupField ,this is a must:
DataView myview = usersDatatable.DefaultView; myview.Sort = "UserType asc" ; ddlUsers.DataSource = myview; ddlUsers.DataTextField = "Username" ; ddlUsers.DataValueField = "Id" ; ddlUsers.DataGroupField = "UserType" ; ddlUsers.DataBind(); |
DataGoupField must contains the name of the column that it will be used as a group title
so the data in the groupDropDownList will show your data like this:
Pretty cool code but wait a second ?! Explain to me the code in our class library:
A) What is DataGroupField ? :
we created a property and named it "DataGroupField" (yeah,I tried to name it something similar to those DataTextField and DataValueField to make it more appropriate) and make it appears as a property in group "Data",but I needed to store it's value in a viewstate so that I will not lose it in postbacks,and it's value by default is empty string.
B) What does IsGroupHasEnabledItems do ? :
You may want to disable some items and that's fine,however if you disabled all items of a group then you don't want to render that Title of the group at all,so the main purpose of this method is to check if that group still has any enabled items or not
C) Why did we overrided RenderContents ? :
We need to override how our control is rendered,as you remember we need grouping so we need to customize how our control is rendered to achieve the same results we achieved in our sample of "select" tag.
please read my comments in that method
D:) Why did we overrided OnDataBinding ? :
This function is called on binding before calling RenderContent,but in RenderContent I will never know the current item's group which it's going to be rendered,so that's gave me an idea..I can loop within my items and check if that property (DataGroupField) is not empty then you need grouping,but I need to store an attribute on the item so I can read from it later on in RenderContents method to know to which group is that item..tricky right
E:) What does that GetResolvedDataSource do ? :
Just to check if the datasource is valid or not
F:) Why did we overrided SaveViewState and LoadViewState ? :
When the control postback or refreshed,control will be rendered once again but without binding(without calling OnDataBinding),which means that our items will be without attributes(if you remember,OnDataBinding is the one who adds the attribute DataGroupField which later on is used in RenderContents method and used to define item's group) and thus the grouping will not be rendered.
so what's the solution? we need to override how control's viewstate is saved and retrieved,WHY? because we want to store the attribute into viewstate and load it once again,that way when the control is rendered,our items will keep their attributes within it's viewstate and thus the grouping will be rendered..do you find it tricky?
FAQ:
1) I'm using DataReader,not DataTable so what should I do?
You need to fill your data from the datareader into a temporary datatable object,then use a DataView to sort your data then use it as a datasource for your control
2) What if I want to add an item within a group,can I do that?
Do it like this:
ListItem egypt = new ListItem( "Egypt" , "4" ); egypt.Attributes.Add( "DataGroupField" , "Africa Countries" ); GroupDropDownList1.Items.Add(egypt); |
Credits:
I want to thank Scott Mitchell for his great article "ListControl Items, Attributes, and ViewState" and I give him the creadit for using his code "LoadViewState and SaveViewState".
Also I want to thank lotuspro for his ideas in his article "ASP.NET DropDownList with OptionGroup support"