|
Introduction
ASP.NET 2.0, for all its bells and whistles, lacks the odd bit of functionality for reasons completely unknown. One such notable omission is that of OptionGroup (<optgroup>) support in the DropDownList control. For those unfamiliar with the <optgroup> element, it is part of the XHTML standard, and has the effect of categorising items in a <select>, as the following image shows.

When I first began to look for a solution to implement this in ASP.NET, I found very few articles on the topic that offered any viable solution. Another CodeProject member came up with a nice clean solution, but it looked like a lot of code and I was convinced there was an easier way. After reading some comments, it became apparent a Control Adapter was the way to go. Control Adapters are new in ASP.NET 2.0, and allow the developer to override the rendering behaviour of any control, very powerful stuff! Furthermore, Control Adapters are used in conjunction with a browser file, so specific browsers may be targeted, if required. Armed with that knowledge, the solution became simple. The attached download contains the requisite files to implement this solution in your own projects, but for posterity, I paste it here also. public class DropDownListAdapter :
System.Web.UI.WebControls.Adapters.WebControlAdapter {
protected override void RenderContents(HtmlTextWriter writer) {
DropDownList list = this.Control as DropDownList;
string currentOptionGroup;
List<string> renderedOptionGroups = new List<string>();
foreach(ListItem item in list.Items) {
if(item.Attributes["OptionGroup"] == null) {
RenderListItem(item, writer);
} else {
currentOptionGroup = item.Attributes["OptionGroup"];
if(renderedOptionGroups.Contains(currentOptionGroup)) {
RenderListItem(item, writer);
} else {
if(renderedOptionGroups.Count > 0) {
RenderOptionGroupEndTag(writer);
}
RenderOptionGroupBeginTag(currentOptionGroup,
writer);
renderedOptionGroups.Add(currentOptionGroup);
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();
}
}
| You must Sign In to use this message board. |
|
| | Msgs 1 to 25 of 56 (Total in Forum: 56) (Refresh) | FirstPrevNext |
|
|
 |
|
|
I am using this adapter for listbox. Everything works fine except that optgroup structure disappears after PostBack. When I used following two functions for saving and loading viewstate it gives me error CS0115: 'ListAdapter.ListBoxAdapter.SaveViewState()': no suitable method found to override
The code I added for saving and loading viewstate is: 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 object[] state = new object[this.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 < this.Items.Count; i++) { if (this.Items[i].Attributes.Count > 0) { itemHasAttributes = true;
// Create an array of the item's Attribute's keys and values object[] attribKV = new object[this.Items[i].Attributes.Count * 2]; int k = 0; foreach (string key in this.Items[i].Attributes.Keys) { attribKV[k++] = key; attribKV[k++] = this.Items[i].Attributes[key]; }
state[i + 1] = attribKV; } }
// return either baseState or state, depending on whether or not // any ListItems had attributes if (itemHasAttributes) return state; else return baseState; }
protected override void LoadViewState(object savedState) { if (savedState == null) return;
// see if savedState is an object or object array if (savedState is object[]) { // we have an array of items with attributes object[] state = (object[])savedState; 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 object[] attribKV = (object[])state[i]; for (int k = 0; k < attribKV.Length; k += 2) this.Items[i - 1].Attributes.Add(attribKV[k].ToString(), attribKV[k + 1].ToString()); } } } else // we have just the base state base.LoadViewState(savedState); }
Is there any complete solution for this?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
"OnSelectedIndexChanged" is not working.Can any one suggest what changes i have to do in my code to use this event of dropdown list.
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
Hi trying to use the "OnSelectedIndexChanged" event but it is not working.Can any one suggest what changes i have to do in my code to use this event of dropdown list.
|
| Sign In·View Thread·PermaLink | 3.25/5 (4 votes) |
|
|
|
 |
|
|
 |
|
|
Is it possible to make more than one level item into the dropdown list? And user will be able to select any item from the list. eg.,
Item1 --Item11 ----Item111 ----Item112 --Item12 ----Item121 ----Item122 --Item13 Item2 --Item21 --Item22 Item3 Item4
|
| Sign In·View Thread·PermaLink | 2.75/5 (3 votes) |
|
|
|
 |
|
|
hi,
i must say that this control is excellent - i m only facing problem in changing font style to normal (not italic) of 'optgroup' element.
i.e. the OptionGroup like 'Mammals' is in italic .. i want it to change to Normal style.
please help
regards asif
|
| Sign In·View Thread·PermaLink | 3.00/5 (1 vote) |
|
|
|
 |
|
|
I've tried several ideas to get around this but it seems the default browser behaviour in IE cannot be overidden. In theory, the following CSS should do it:
select optgroup { font-style: normal; }
But this only seems to work in FireFox.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi,
Following my previous post, since there is no known solution for the Viewstate issue, i decided to use the list as a control.
I did find out do that the items have to be added in grouping order, this cause a problem to me.
So below is a fix so you can add items in any order, note that items without a group will always be added on top.
[DefaultValue(true)] public bool SortItems { get { return ViewState["SortItems"] == null ? true : (bool)ViewState["SortItems"]; } set { ViewState["SortItems"] = value; } } protected override void RenderContents(HtmlTextWriter writer) { SortedDictionary> sortedItems = new SortedDictionary>();
foreach (ListItem item in this.Items) { if (item.Attributes[OptionGroupAttribute] == null) { if (sortedItems.ContainsKey("_")) { sortedItems["_"].Add(item); } else { sortedItems.Add("_", new List()); sortedItems["_"].Add(item); } } else { string optionGroup = item.Attributes[OptionGroupAttribute];
if (sortedItems.ContainsKey(optionGroup)) { sortedItems[optionGroup].Add(item); } else { sortedItems.Add(optionGroup, new List()); sortedItems[optionGroup].Add(item); } } }
foreach (KeyValuePair> entry in sortedItems) { if (SortItems) { entry.Value.Sort(delegate(ListItem item1, ListItem item2) { return item1.Text.CompareTo(item2.Text); }); }
if (entry.Key != "_") RenderOptionGroupBeginTag(entry.Key, writer);
foreach (ListItem item in entry.Value) { Page.ClientScript.RegisterForEventValidation(this.UniqueID, item.Value);
RenderListItem(item, writer); }
if (entry.Key != "_") RenderOptionGroupEndTag(writer); } }
And also here is a fix for the viewstate issues:
protected override object SaveViewState() { this.ViewState["-1Saved"] = (this.SelectedIndex == -1);
for (int i = 0; i < this.Items.Count; i++) { if (this.Items[i].Attributes[OptionGroupAttribute] != null) this.ViewState["ListItemGroup" + i] = this.Items[i].Attributes[OptionGroupAttribute]; } return base.SaveViewState(); }
protected override void LoadViewState(object state) {
base.LoadViewState(state);
for (int i = 0; i < this.Items.Count; i++) { if (this.ViewState["ListItemGroup" + i] != null) this.Items[i].Attributes[OptionGroupAttribute] = (string)this.ViewState["ListItemGroup" + i]; }
if (this.ViewState["-1Saved"] != null && (Boolean)this.ViewState["-1Saved"]) { this.SelectedIndex = -1; } }
|
| Sign In·View Thread·PermaLink | 3.00/5 (2 votes) |
|
|
|
 |
|
|
I used Albert Weinert's code below to show my optgroups, everything works fine until the page postbacks. The list gets added again making duplicates and each time there is a post back the list grows. What could be wrong?
|
| Sign In·View Thread·PermaLink | 3.00/5 (1 vote) |
|
|
|
 |
|
|
Please see my fixes on a post above!
The list is being stored in viewstate, you shouldn't have to re-populate after every postback:
For example:
protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { PopulateListWithValues(); } }
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
For anyone in need, here is a VB version of the class:
For anyone in need for a VB version:
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 ArrayList
Dim item As ListItem For Each item In list.Items If item.Attributes("OptionGroup") Is Nothing Then RenderListItem(item, writer) Else currentOptionGroup = item.Attributes("OptionGroup")
If renderedOptionGroups.Contains(currentOptionGroup) Then RenderListItem(item, writer) Else If renderedOptionGroups.Count > 0 Then RenderOptionGroupEndTag(writer) End If
RenderOptionGroupBeginTag(currentOptionGroup, writer) renderedOptionGroups.Add(currentOptionGroup)
RenderListItem(item, writer) End If End If Next
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
Dim key As String For Each key 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
Jaywon
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
...and a couple of other minor alterations:
Imports System Imports System.Data Imports System.Configuration Imports System.Web Imports System.Web.Security Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.Web.UI.WebControls.WebParts Imports System.Web.UI.HtmlControls Imports System.Collections Imports System.Collections.Generic
Public Class DropDownListAdapter Inherits System.Web.UI.WebControls.Adapters.WebControlAdapter
Private Const m_optionGroupAttribute As String = "OptionGroup" Private Const m_tagOptionGroup As String = "optgroup" Private Const m_attributeLabel As String = "label" Private m_viewstates As Object()
Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) If (Not Page Is Nothing) Then Dim l_list As DropDownList = CType(Me.Control, DropDownList) Dim l_currentOptionGroup As String Dim l_renderedOptionGroups As New ArrayList()
For Each l_item As ListItem In l_list.Items Page.ClientScript.RegisterForEventValidation(l_list.UniqueID, l_item.Value)
If l_item.Attributes(m_optionGroupAttribute) Is Nothing Then RenderListItem(l_item, writer) Else l_currentOptionGroup = l_item.Attributes(m_optionGroupAttribute)
If (l_renderedOptionGroups.Contains(l_currentOptionGroup)) Then RenderListItem(l_item, writer) Else If (l_renderedOptionGroups.Count > 0) Then RenderOptionGroupEndTag(writer) End If RenderOptionGroupBeginTag(l_currentOptionGroup, writer) l_renderedOptionGroups.Add(l_currentOptionGroup) RenderListItem(l_item, writer) End If End If Next
If (l_renderedOptionGroups.Count > 0) Then RenderOptionGroupEndTag(writer) End If Else MyBase.RenderContents(writer) End If End Sub
Private Sub RenderOptionGroupBeginTag(ByVal name As String, ByVal writer As HtmlTextWriter) writer.AddAttribute(m_attributeLabel, name) writer.RenderBeginTag(m_tagOptionGroup) End Sub
Private Sub RenderOptionGroupEndTag(ByVal writer As HtmlTextWriter) writer.RenderEndTag() End Sub
Private Sub RenderListItem(ByVal item As ListItem, ByVal writer As HtmlTextWriter) For Each key As String In item.Attributes.Keys If (key <> m_optionGroupAttribute) Then writer.AddAttribute(key, item.Attributes(key)) End If Next
writer.AddAttribute(HtmlTextWriterAttribute.Value, item.Value, True)
If (item.Selected) Then writer.AddAttribute(HtmlTextWriterAttribute.Selected, "selected") End If
writer.RenderBeginTag(HtmlTextWriterTag.Option) writer.WriteEncodedText(item.Text) writer.RenderEndTag() End Sub
Protected Overrides Function SaveAdapterViewState() As Object If (Not Page Is Nothing) Then Dim l_list As DropDownList = CType(Control, DropDownList) Dim l_viewState(l_list.Items.Count + 1) As Object
Dim i As Integer = 0 For Each item As ListItem In l_list.Items
l_viewState(i) = item.Attributes(m_optionGroupAttribute) i += 1 Next
l_viewState(i) = MyBase.SaveAdapterViewState() Return l_viewState Else Return MyBase.SaveAdapterControlState() End If End Function
Protected Overrides Sub LoadAdapterViewState(ByVal state As Object) If (Not Page Is Nothing) Then m_viewstates = CType(state, Object()) MyBase.LoadAdapterViewState(m_viewstates(m_viewstates.Length - 1)) Else MyBase.LoadAdapterViewState(state) End If End Sub
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs) If (Not Page Is Nothing) Then If (Not m_viewstates Is Nothing AndAlso m_viewstates.Length > 1) Then Dim l_list As DropDownList = CType(Control, DropDownList) If (Page.EnableEventValidation) Then If (m_viewstates.Length <> l_list.Items.Count + 1) Then Throw New ViewStateException() End If End If
Dim l_max As Integer = m_viewstates.Length If (l_list.Items.Count < l_max) Then l_max = l_list.Items.Count End If
For i As Integer = 0 To l_max - 1 l_list.Items(i).Attributes(m_optionGroupAttribute) = CStr(m_viewstates(i)) Next End If End If
MyBase.OnPreRender(e) End Sub End Class
Marc
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
I modified the code above to make a standalone version.
Now you can add this class to a Web Control Library to use it as a web control.
Here's the code (enjoy):
Imports System.Web.UI Imports System.Web.UI.WebControls
Public Class ComboBox Inherits DropDownList
Private Const m_optionGroupAttribute As String = "OptionGroup" Private Const m_tagOptionGroup As String = "optgroup" Private Const m_attributeLabel As String = "label" Private m_viewstates As Object()
Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) If (Not Page Is Nothing) Then Dim l_list As DropDownList = CType(Me, DropDownList) Dim l_currentOptionGroup As String Dim l_renderedOptionGroups As New ArrayList()
For Each l_item As ListItem In l_list.Items Page.ClientScript.RegisterForEventValidation(l_list.UniqueID, l_item.Value)
If l_item.Attributes(m_optionGroupAttribute) Is Nothing Then RenderListItem(l_item, writer) Else l_currentOptionGroup = l_item.Attributes(m_optionGroupAttribute)
If (l_renderedOptionGroups.Contains(l_currentOptionGroup)) Then RenderListItem(l_item, writer) Else If (l_renderedOptionGroups.Count > 0) Then RenderOptionGroupEndTag(writer) End If RenderOptionGroupBeginTag(l_currentOptionGroup, writer) l_renderedOptionGroups.Add(l_currentOptionGroup) RenderListItem(l_item, writer) End If End If Next
If (l_renderedOptionGroups.Count > 0) Then RenderOptionGroupEndTag(writer) End If Else MyBase.RenderContents(writer) End If End Sub
Private Sub RenderOptionGroupBeginTag(ByVal name As String, ByVal writer As HtmlTextWriter) writer.AddAttribute(m_attributeLabel, name) writer.RenderBeginTag(m_tagOptionGroup) End Sub
Private Sub RenderOptionGroupEndTag(ByVal writer As HtmlTextWriter) writer.RenderEndTag() End Sub
Private Sub RenderListItem(ByVal item As ListItem, ByVal writer As HtmlTextWriter) For Each key As String In item.Attributes.Keys If (key <> m_optionGroupAttribute) Then writer.AddAttribute(key, item.Attributes(key)) End If Next
writer.AddAttribute(HtmlTextWriterAttribute.Value, item.Value, True)
If (item.Selected) Then writer.AddAttribute(HtmlTextWriterAttribute.Selected, "selected") End If
writer.RenderBeginTag(HtmlTextWriterTag.Option) writer.WriteEncodedText(item.Text) writer.RenderEndTag() End Sub
Protected Overrides Function SaveViewState() As Object If (Not Page Is Nothing) Then Dim l_list As DropDownList = CType(Me, DropDownList) Dim l_viewState(l_list.Items.Count) As Object ' Note: Count + 1 caused eventvalidation errors.
Dim i As Integer = 0 For Each item As ListItem In l_list.Items
l_viewState(i) = item.Attributes(m_optionGroupAttribute) i += 1 Next
l_viewState(i) = MyBase.SaveViewState() Return l_viewState Else Return MyBase.SaveViewState() End If End Function
Protected Overrides Sub LoadViewState(ByVal state As Object) If (Not Page Is Nothing) Then m_viewstates = CType(state, Object()) MyBase.LoadViewState(m_viewstates(m_viewstates.Length - 1)) Else MyBase.LoadViewState(state) End If End Sub
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs) If (Not Page Is Nothing) Then If (Not m_viewstates Is Nothing AndAlso m_viewstates.Length > 1) Then Dim l_list As DropDownList = CType(Me, DropDownList) If (Page.EnableEventValidation) Then If (m_viewstates.Length <> l_list.Items.Count + 1) Then Throw New ViewStateException() End If End If
Dim l_max As Integer = m_viewstates.Length If (l_list.Items.Count < l_max) Then l_max = l_list.Items.Count End If
For i As Integer = 0 To l_max - 1 l_list.Items(i).Attributes(m_optionGroupAttribute) = CStr(m_viewstates(i)) Next End If End If
MyBase.OnPreRender(e) End Sub End Class
Note that I modified something in the SaveViewState function.
-- modified at 13:22 Thursday 4th October, 2007
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
WOWZERS, After a few hours of putzing about, I finally got this working. It appears that the + 1 in the SaveViewState IS REQUIRED! othewise an index out of bounds error occurs.
I REALLY appreciate you all getting this working, thanks a bunch!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hello, You example is great and just what I want. But if you use your dropdown box in a usercontrol and add it to a page with Ajax wrapped around it. it fires off an error due to I think the HttpModules. Any idea how to get around this ?
David
-- modified at 5:47 Wednesday 21st March, 2007
|
| Sign In·View Thread·PermaLink | 1.63/5 (4 votes) |
|
|
|
 |
|
|
I'm working with VS2005 but when I Build My Web Site it always shows this error. What should I do? Thanks!
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
 |
|
|
 |
|
|
I had the same error come up when I tried it. I found the problem to be in the Page Directive of the HTML page. The following line:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="DropDownListOptionGroup.aspx.cs" Inherits="CraigBlog.Net.DropDownListOptionGroup" %>
Should be changed to:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="DropDownListOptionGroup.aspx.cs" Inherits="CraigBlog.Net.TestDropDownList" %>
Specifically, the Inherits attribute is given the wrong value. The page file name may be "DropDownListOptionGroup" but the partial class name is "TestDropDownList". Making that change, all was right in the world once more.
-- Andrew
|
| Sign In·View Thread·PermaLink | 2.00/5 (2 votes) |
|
|
|
 |
|
|
Hi,
I'm wanting to make this work in FireFox too - any clues as to what I need to be tinkering with? I assume I need to add something to the browser file? How does the code reference the browser file?
Thanks in advance
V
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
This link doesn't work currently. This is the control that would work great but we need it to work in FireFox as well. Any one know what I would need to do to fix this.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
| | |