Dropdownlist control with <optgroup>s for asp.net (webforms)?
Solution 1
I've used the standard control in the past, and just added a simple ControlAdapter for it that would override the default behavior so it could render <optgroup>s in certain places. This works great even if you have controls that don't need the special behavior, because the additional feature doesn't get in the way.
Note that this was for a specific purpose and written in .Net 2.0, so it may not suit you as well, but it should at least give you a starting point. Also, you have to hook it up using a .browserfile in your project (see the end of the post for an example).
'This codes makes the dropdownlist control recognize items with "--"
'for the label or items with an OptionGroup attribute and render them
'as <optgroup> instead of <option>.
Public Class DropDownListAdapter
Inherits System.Web.UI.WebControls.Adapters.WebControlAdapter
Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
Dim list As DropDownList = Me.Control
Dim currentOptionGroup As String
Dim renderedOptionGroups As New Generic.List(Of String)
For Each item As ListItem In list.Items
Page.ClientScript.RegisterForEventValidation(list.UniqueID, item.Value)
If item.Attributes("OptionGroup") IsNot Nothing Then
'The item is part of an option group
currentOptionGroup = item.Attributes("OptionGroup")
If Not renderedOptionGroups.Contains(currentOptionGroup) Then
'the header was not written- do that first
'TODO: make this stack-based, so the same option group can be used more than once in longer select element (check the most-recent stack item instead of anything in the list)
If (renderedOptionGroups.Count > 0) Then
RenderOptionGroupEndTag(writer) 'need to close previous group
End If
RenderOptionGroupBeginTag(currentOptionGroup, writer)
renderedOptionGroups.Add(currentOptionGroup)
End If
RenderListItem(item, writer)
ElseIf item.Text = "--" Then 'simple separator
RenderOptionGroupBeginTag("--", writer)
RenderOptionGroupEndTag(writer)
Else
'default behavior: render the list item as normal
RenderListItem(item, writer)
End If
Next item
If renderedOptionGroups.Count > 0 Then
RenderOptionGroupEndTag(writer)
End If
End Sub
Private Sub RenderOptionGroupBeginTag(ByVal name As String, ByVal writer As HtmlTextWriter)
writer.WriteBeginTag("optgroup")
writer.WriteAttribute("label", name)
writer.Write(HtmlTextWriter.TagRightChar)
writer.WriteLine()
End Sub
Private Sub RenderOptionGroupEndTag(ByVal writer As HtmlTextWriter)
writer.WriteEndTag("optgroup")
writer.WriteLine()
End Sub
Private Sub RenderListItem(ByVal item As ListItem, ByVal writer As HtmlTextWriter)
writer.WriteBeginTag("option")
writer.WriteAttribute("value", item.Value, True)
If item.Selected Then
writer.WriteAttribute("selected", "selected", False)
End If
For Each key As String In item.Attributes.Keys
writer.WriteAttribute(key, item.Attributes(key))
Next key
writer.Write(HtmlTextWriter.TagRightChar)
HttpUtility.HtmlEncode(item.Text, writer)
writer.WriteEndTag("option")
writer.WriteLine()
End Sub
End Class
Here's a C# implementation of the same Class:
/* This codes makes the dropdownlist control recognize items with "--"
* for the label or items with an OptionGroup attribute and render them
* as <optgroup> instead of <option>.
*/
public class DropDownListAdapter : WebControlAdapter
{
protected override void RenderContents(HtmlTextWriter writer)
{
//System.Web.HttpContext.Current.Response.Write("here");
var list = (DropDownList)this.Control;
string currentOptionGroup;
var renderedOptionGroups = new List<string>();
foreach (ListItem item in list.Items)
{
Page.ClientScript.RegisterForEventValidation(list.UniqueID, item.Value);
//Is the item part of an option group?
if (item.Attributes["OptionGroup"] != null)
{
currentOptionGroup = item.Attributes["OptionGroup"];
//Was the option header already written, then just render the list item
if (renderedOptionGroups.Contains(currentOptionGroup))
RenderListItem(item, writer);
//The header was not written,do that first
else
{
//Close previous group
if (renderedOptionGroups.Count > 0)
RenderOptionGroupEndTag(writer);
RenderOptionGroupBeginTag(currentOptionGroup, writer);
renderedOptionGroups.Add(currentOptionGroup);
RenderListItem(item, writer);
}
}
//Simple separator
else if (item.Text == "--")
{
RenderOptionGroupBeginTag("--", writer);
RenderOptionGroupEndTag(writer);
}
//Default behavior, render the list item as normal
else
RenderListItem(item, writer);
}
if (renderedOptionGroups.Count > 0)
RenderOptionGroupEndTag(writer);
}
private void RenderOptionGroupBeginTag(string name, HtmlTextWriter writer)
{
writer.WriteBeginTag("optgroup");
writer.WriteAttribute("label", name);
writer.Write(HtmlTextWriter.TagRightChar);
writer.WriteLine();
}
private void RenderOptionGroupEndTag(HtmlTextWriter writer)
{
writer.WriteEndTag("optgroup");
writer.WriteLine();
}
private void RenderListItem(ListItem item, HtmlTextWriter writer)
{
writer.WriteBeginTag("option");
writer.WriteAttribute("value", item.Value, true);
if (item.Selected)
writer.WriteAttribute("selected", "selected", false);
foreach (string key in item.Attributes.Keys)
writer.WriteAttribute(key, item.Attributes[key]);
writer.Write(HtmlTextWriter.TagRightChar);
HttpUtility.HtmlEncode(item.Text, writer);
writer.WriteEndTag("option");
writer.WriteLine();
}
}
My browser file was named "App_Browsers\BrowserFile.browser" and looked like this:
<!--
You can find existing browser definitions at
<windir>\Microsoft.NET\Framework\<ver>\CONFIG\Browsers
-->
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.WebControls.DropDownList"
adapterType="DropDownListAdapter" />
</controlAdapters>
</browser>
</browsers>
Solution 2
I have used JQuery to achieve this task. I first added an new attribute for every ListItem
from the backend and then used that attribute in JQuery wrapAll()
method to create groups...
C#:
foreach (ListItem item in ((DropDownList)sender).Items)
{
if (System.Int32.Parse(item.Value) < 5)
item.Attributes.Add("classification", "LessThanFive");
else
item.Attributes.Add("classification", "GreaterThanFive");
}
JQuery:
$(document).ready(function() {
//Create groups for dropdown list
$("select.listsmall option[@classification='LessThanFive']")
.wrapAll("<optgroup label='Less than five'>");
$("select.listsmall option[@classification='GreaterThanFive']")
.wrapAll("<optgroup label='Greater than five'>");
});
Solution 3
Thanks Joel! everyone... here's C# version if you want it:
using System;
using System.Web.UI.WebControls.Adapters;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Generic;
using System.Web;
//This codes makes the dropdownlist control recognize items with "--"'
//for the label or items with an OptionGroup attribute and render them'
//as instead of .'
public class DropDownListAdapter : WebControlAdapter
{
protected override void RenderContents(HtmlTextWriter writer)
{
DropDownList list = Control as DropDownList;
string currentOptionGroup;
List renderedOptionGroups = new List();
foreach(ListItem item in list.Items)
{
if (item.Attributes["OptionGroup"] != null)
{
//'The item is part of an option group'
currentOptionGroup = item.Attributes["OptionGroup"];
//'the option header was already written, just render the list item'
if(renderedOptionGroups.Contains(currentOptionGroup))
RenderListItem(item, writer);
else
{
//the header was not written- do that first'
if (renderedOptionGroups.Count > 0)
RenderOptionGroupEndTag(writer); //'need to close previous group'
RenderOptionGroupBeginTag(currentOptionGroup, writer);
renderedOptionGroups.Add(currentOptionGroup);
RenderListItem(item, writer);
}
}
else if (item.Text == "--") //simple separator
{
RenderOptionGroupBeginTag("--", writer);
RenderOptionGroupEndTag(writer);
}
else
{
//default behavior: render the list item as normal'
RenderListItem(item, writer);
}
}
if(renderedOptionGroups.Count > 0)
RenderOptionGroupEndTag(writer);
}
private void RenderOptionGroupBeginTag(string name, HtmlTextWriter writer)
{
writer.WriteBeginTag("optgroup");
writer.WriteAttribute("label", name);
writer.Write(HtmlTextWriter.TagRightChar);
writer.WriteLine();
}
private void RenderOptionGroupEndTag(HtmlTextWriter writer)
{
writer.WriteEndTag("optgroup");
writer.WriteLine();
}
private void RenderListItem(ListItem item, HtmlTextWriter writer)
{
writer.WriteBeginTag("option");
writer.WriteAttribute("value", item.Value, true);
if (item.Selected)
writer.WriteAttribute("selected", "selected", false);
foreach (string key in item.Attributes.Keys)
writer.WriteAttribute(key, item.Attributes[key]);
writer.Write(HtmlTextWriter.TagRightChar);
HttpUtility.HtmlEncode(item.Text, writer);
writer.WriteEndTag("option");
writer.WriteLine();
}
}
Solution 4
The above code renders the end tag for the optgroup before any of the options, so the options don't get indented like they should in addition to the markup not properly representing the grouping. Here's my slightly modified version of Tom's code:
public class ExtendedDropDownList : System.Web.UI.WebControls.DropDownList
{
public const string OptionGroupTag = "optgroup";
private const string OptionTag = "option";
protected override void RenderContents(System.Web.UI.HtmlTextWriter writer)
{
ListItemCollection items = this.Items;
int count = items.Count;
string tag;
string optgroupLabel;
if (count > 0)
{
bool flag = false;
string prevOptGroup = null;
for (int i = 0; i < count; i++)
{
tag = OptionTag;
optgroupLabel = null;
ListItem item = items[i];
if (item.Enabled)
{
if (item.Attributes != null && item.Attributes.Count > 0 && item.Attributes[OptionGroupTag] != null)
{
optgroupLabel = item.Attributes[OptionGroupTag];
if (prevOptGroup != optgroupLabel)
{
if (prevOptGroup != null)
{
writer.WriteEndTag(OptionGroupTag);
}
writer.WriteBeginTag(OptionGroupTag);
if (!string.IsNullOrEmpty(optgroupLabel))
{
writer.WriteAttribute("label", optgroupLabel);
}
writer.Write('>');
}
item.Attributes.Remove(OptionGroupTag);
prevOptGroup = optgroupLabel;
}
else
{
if (prevOptGroup != null)
{
writer.WriteEndTag(OptionGroupTag);
}
prevOptGroup = null;
}
writer.WriteBeginTag(tag);
if (item.Selected)
{
if (flag)
{
this.VerifyMultiSelect();
}
flag = true;
writer.WriteAttribute("selected", "selected");
}
writer.WriteAttribute("value", item.Value, true);
if (item.Attributes != null && item.Attributes.Count > 0)
{
item.Attributes.Render(writer);
}
if (optgroupLabel != null)
{
item.Attributes.Add(OptionGroupTag, optgroupLabel);
}
if (this.Page != null)
{
this.Page.ClientScript.RegisterForEventValidation(this.UniqueID, item.Value);
}
writer.Write('>');
HttpUtility.HtmlEncode(item.Text, writer);
writer.WriteEndTag(tag);
writer.WriteLine();
if (i == count - 1)
{
if (prevOptGroup != null)
{
writer.WriteEndTag(OptionGroupTag);
}
}
}
}
}
}
protected override object SaveViewState()
{
object[] state = new object[this.Items.Count + 1];
object baseState = base.SaveViewState();
state[0] = baseState;
bool itemHasAttributes = false;
for (int i = 0; i < this.Items.Count; i++)
{
if (this.Items[i].Attributes.Count > 0)
{
itemHasAttributes = true;
object[] attributes = new object[this.Items[i].Attributes.Count * 2];
int k = 0;
foreach (string key in this.Items[i].Attributes.Keys)
{
attributes[k] = key;
k++;
attributes[k] = this.Items[i].Attributes[key];
k++;
}
state[i + 1] = attributes;
}
}
if (itemHasAttributes)
return state;
return baseState;
}
protected override void LoadViewState(object savedState)
{
if (savedState == null)
return;
if (!(savedState.GetType().GetElementType() == null) &&
(savedState.GetType().GetElementType().Equals(typeof(object))))
{
object[] state = (object[])savedState;
base.LoadViewState(state[0]);
for (int i = 1; i < state.Length; i++)
{
if (state[i] != null)
{
object[] attributes = (object[])state[i];
for (int k = 0; k < attributes.Length; k += 2)
{
this.Items[i - 1].Attributes.Add
(attributes[k].ToString(), attributes[k + 1].ToString());
}
}
}
}
else
{
base.LoadViewState(savedState);
}
}
}
Use it like this:
ListItem item1 = new ListItem("option1");
item1.Attributes.Add("optgroup", "CatA");
ListItem item2 = new ListItem("option2");
item2.Attributes.Add("optgroup", "CatA");
ListItem item3 = new ListItem("option3");
item3.Attributes.Add("optgroup", "CatB");
ListItem item4 = new ListItem("option4");
item4.Attributes.Add("optgroup", "CatB");
ListItem item5 = new ListItem("NoOptGroup");
ddlTest.Items.Add(item1);
ddlTest.Items.Add(item2);
ddlTest.Items.Add(item3);
ddlTest.Items.Add(item4);
ddlTest.Items.Add(item5);
and here's the generated markup (indented for ease of viewing):
<select name="ddlTest" id="Select1">
<optgroup label="CatA">
<option selected="selected" value="option1">option1</option>
<option value="option2">option2</option>
</optgroup>
<optgroup label="CatB">
<option value="option3">option3</option>
<option value="option4">option4</option>
</optgroup>
<option value="NoOptGroup">NoOptGroup</option>
</select>
Solution 5
I use the reflector to see why is not supported. There is why. In the render method of the ListControl no condition is there to create the optgroup.
protected internal override void RenderContents(HtmlTextWriter writer)
{
ListItemCollection items = this.Items;
int count = items.Count;
if (count > 0)
{
bool flag = false;
for (int i = 0; i < count; i++)
{
ListItem item = items[i];
if (item.Enabled)
{
writer.WriteBeginTag("option");
if (item.Selected)
{
if (flag)
{
this.VerifyMultiSelect();
}
flag = true;
writer.WriteAttribute("selected", "selected");
}
writer.WriteAttribute("value", item.Value, true);
if (item.HasAttributes)
{
item.Attributes.Render(writer);
}
if (this.Page != null)
{
this.Page.ClientScript.RegisterForEventValidation(this.UniqueID, item.Value);
}
writer.Write('>');
HttpUtility.HtmlEncode(item.Text, writer);
writer.WriteEndTag("option");
writer.WriteLine();
}
}
}
}
So i create my own dropdown Control with an override of the method RenderContents. There is my control. Is working fine. I use exactly the same code of Microsoft, just add a little condition to support listItem having attribute optgroup to create an optgroup and not a option.
Give me some feed back
public class DropDownListWithOptionGroup : DropDownList
{
public const string OptionGroupTag = "optgroup";
private const string OptionTag = "option";
protected override void RenderContents(System.Web.UI.HtmlTextWriter writer)
{
ListItemCollection items = this.Items;
int count = items.Count;
string tag;
string optgroupLabel;
if (count > 0)
{
bool flag = false;
for (int i = 0; i < count; i++)
{
tag = OptionTag;
optgroupLabel = null;
ListItem item = items[i];
if (item.Enabled)
{
if (item.Attributes != null && item.Attributes.Count > 0 && item.Attributes[OptionGroupTag] != null)
{
tag = OptionGroupTag;
optgroupLabel = item.Attributes[OptionGroupTag];
}
writer.WriteBeginTag(tag);
// NOTE(cboivin): Is optionGroup
if (!string.IsNullOrEmpty(optgroupLabel))
{
writer.WriteAttribute("label", optgroupLabel);
}
else
{
if (item.Selected)
{
if (flag)
{
this.VerifyMultiSelect();
}
flag = true;
writer.WriteAttribute("selected", "selected");
}
writer.WriteAttribute("value", item.Value, true);
if (item.Attributes != null && item.Attributes.Count > 0)
{
item.Attributes.Render(writer);
}
if (this.Page != null)
{
this.Page.ClientScript.RegisterForEventValidation(this.UniqueID, item.Value);
}
}
writer.Write('>');
HttpUtility.HtmlEncode(item.Text, writer);
writer.WriteEndTag(tag);
writer.WriteLine();
}
}
}
}
}
Comments
-
Nick almost 2 years
Can anyone recommend a dropdownlist control for asp.net (3.5) that can render option groups? Thanks
-
Ash Machine over 15 yearsThis is great for overriding all drop down lists in a web application. But is there a similar solution for creating a custom control that can be used only where option groups are needed?
-
Joel Coehoorn over 15 yearsYou could inherit from the dropdownlist control instead, and adapt this code to override the render event for that control. But really, this code shouldn't get in the way of other lists on the page.
-
Glennular over 14 yearsI have added a version that will fix the viewstate bug.
-
Pete over 14 yearsI found this solution through a google search, and I was able to implement it in my existing web app in a few minutes. Brilliant ;)
-
satyavrat about 13 years<%@ Register TagPrefix="MyCompany" Namespace="MyCompany.Framework.Web.CustomControls" Assembly="MyCompany.Framework.Web" %> to register the namespace that the class is in
-
user3001801 about 13 years@Ash Machine - Another option that worked for me was to subclass the System.Web.UI.WebControls.DropDownList control with a wrapper class YourNamespace.DropDownListWithOptionGroups, reference your subclass in the browsers file. Then, whenever you want to use the special dropdown you reference DropDownListWithOptionGroups in your ASPX file instead of DropDownList.
-
sudheshna almost 13 yearsIs there a way I can have the datasource from sql result, I tried the following, ddlCountry.Attributes.Add("OptionGroup", "Region"); I needed to bind coutries with region as the optgroup
-
Mehdi Maujood almost 12 yearsI implemented this, but I'm getting an event validation error on any page with a dropdown after this. Anybody else having this issue?
-
Jon over 11 yearsSame here, I get an event validation error on postback. Will need to look for some other technique.
-
iMatoria over 11 years@cedric-boivin: I had to alter the code slightly to make it work. Also, it would have been better if you could have mentioned how the .aspx page code would look like. Let me know if you require it from me.
-
Chưa biết almost 9 yearsHow to use in aspx.cs?
-
Michael almost 5 years@mhu has a slightly more generic answer that builds on this one. stackoverflow.com/a/21481634/295011