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:GroupDropDownListID="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"