Dropdownlist control with <optgroup>s for asp.net (webforms)?

51,787

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("&lt;optgroup label='Less than five'&gt;");
    $("select.listsmall option[@classification='GreaterThanFive']")
        .wrapAll("&lt;optgroup label='Greater than five'&gt;"); 
});

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();
          }
        }
      }

    }
  }
Share:
51,787
Nick
Author by

Nick

I'm blurry in real life too

Updated on July 08, 2022

Comments

  • Nick
    Nick almost 2 years

    Can anyone recommend a dropdownlist control for asp.net (3.5) that can render option groups? Thanks

  • Ash Machine
    Ash Machine over 15 years
    This 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
    Joel Coehoorn over 15 years
    You 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
    Glennular over 14 years
    I have added a version that will fix the viewstate bug.
  • Pete
    Pete over 14 years
    I 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
    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
    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
    sudheshna almost 13 years
    Is 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
    Mehdi Maujood almost 12 years
    I implemented this, but I'm getting an event validation error on any page with a dropdown after this. Anybody else having this issue?
  • Jon
    Jon over 11 years
    Same here, I get an event validation error on postback. Will need to look for some other technique.
  • iMatoria
    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
    Chưa biết almost 9 years
    How to use in aspx.cs?
  • Michael
    Michael almost 5 years
    @mhu has a slightly more generic answer that builds on this one. stackoverflow.com/a/21481634/295011