jQuery data API and the ASP.net ListControl

Created: August 31, 2016 Tagged As: ASP.net, C#, Web Forms Share:

I am a big fan of jQuery and the data API makes client-side programming much easier since any HTML element can be decorated with custom attributes that contain bits of data. Even using ASP.net WebForms, like I still do for most projects (I know, I know, MVC is Microsoft's flagship and what they are touting as the way to go, but until I can code as fast or faster using its toolset I will continue to use WebForms - with little or no ViewState), adding data attributes is usually quite easy.

However, the list-based controls (DropDownList, CheckBoxList, RadioButtonList) are tough since there is no easy way to decorate each item. You can easily add data bits to the wrapper, but that doesn't do much good as most of the time the data that is truly needed pertains to each individual item. The code below, however, makes adding data attributes to the items almost trivial.

DataApiBindableAttribute

The first place to start is to define how each data attribute will be created. That is the job of the DataApiBindableAttribute class. It specifies the name to output to HTML and the name of the runtime object's property to use for the actual value.

/// <summary>
/// Represents a single bindable jQuery data attribute
/// </summary>
public class DataApiBindableAttribute
{
    /// <summary>
    /// Gets or sets the attribute name (with or without the data- prefix)
    /// </summary>
    public string Name
    {
        get;
        set;
    }
 
    /// <summary>
    /// Gets or sets the name of the field to pull the value from
    /// </summary>
    public string DataValueField
    {
        get;
        set;
    }
}

 

DataApiBindableAttributeCollection

Now that we've defined a single attribute, we need to be able to store a collection of them should we need to include multiple attributes. And in this case, our collection also does the work of adding the jQuery data API attributes to each list item. Through the power of Reflection we can query each object that is bound to the List control, extract the necessary value(s) and add the required data attribute(s).

/// <summary>
/// A list of attributes that can be added via data binding
/// </summary>
public class DataApiBindableAttributeCollection : List<DataApiBindableAttribute>
{
    /// <summary>
    /// Binds this collection to a ListControl
    /// </summary>
    /// <param name="List"></param>
    public void BindToList(ListControl List)
    {
        IEnumerable<object> en = List.DataSource as IEnumerable<object>;
        if (en != null)
        {
            Dictionary<DataApiBindableAttribute, PropertyInfo> props = null;
            int itmIndex = 0;
            foreach (object o in en)
            {
                if (props == null)
                {
                    PropertyInfo[] allProps = o.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);
                    props = new Dictionary<DataApiBindableAttribute, PropertyInfo>();
                    foreach (DataApiBindableAttribute attr in this)
                        props.Add(attr, allProps.Where(pi => pi.Name.Equals(attr.DataValueField, StringComparison.OrdinalIgnoreCase)).FirstOrDefault());
                }
 
                ListItem itm = List.Items[itmIndex++];
                foreach (DataApiBindableAttribute attr in props.Keys)
                {
                    if (props[attr] == null)
                        continue;
                    object val = props[attr].GetValue(o);
                    if (val == null)
                        continue;
                    itm.Attributes.Add($"{(attr.Name.StartsWith("data-") ? "" : "data-")}{attr.Name}", val.ToString());
                }
            }
        }
    }
}

 

The keys here are:

  1. Grab a PropertyInfo array of all public instance properties that have a getter (i.e, declared as public object thing {get;}. They can also have a setter, but we don't care.
  2. As each property is grabbed, we check its name to see if it matches any DataValueField in the list. If so, we associate the DataApiBindableAttribute with the PropertyInfo via a Dictionary.
  3. Finally, as we loop through each in the data source, we grab the corresponding ListItem, and, if the DataSource object's value is non-null we add the data attribute to the Attributes collection so that the ASP.net engine can render it for us.

Example

Now let's see this in action. We will first define a City class that is used for data binding purposes. Then we will create some data, create a jQuery data attribute and bind both to the list.

public class City
{
    public int ID
    {
        get;
        set;
    }
 
    public string CityName
    {
        get;
        set;
    }
 
    public string PostalCode
    {
        get;
        set;
    }
}
 
public class Test
{
    public Test()
    {
        DataApiBindableAttributeCollection attrs = new DataApiBindableAttributeCollection();
        attrs.Add(new DataApiBindableAttribute { Name = "zip", DataValueField = "PostalCode" });
 
        List<City> dataSource = new List<City>();
        dataSource.Add(new City { ID = 1, CityName = "Lakewood", PostalCode = "44107" });
        dataSource.Add(new City { ID = 2, CityName = "Westlake", PostalCode = "44145" });
        dataSource.Add(new City { ID = 1, CityName = "Bay Village", PostalCode = "44140" });
 
        DropDownList list = new DropDownList();
        list.DataValueField = "ID";
        list.DataTextField = "CityName";
        list.DataSource = dataSource;
        list.DataBind();
        attrs.BindToList(list);
 
        using (HtmlTextWriter writer = new HtmlTextWriter(new System.IO.StringWriter()))
        {
            list.RenderControl(writer);
            writer.Flush();
            Console.Write(((System.IO.StringWriter)writer.InnerWriter).ToString());
        }
    }
}

 

The resulting HTML will be:

<select>
    <option value="1" data-zip="44107">Lakewood</option>
    <option value="2" data-zip="44145">Westlake</option>
    <option value="3" data-zip="44140">Bay Village</option>
</select>

 

Wrapping Up

This example shows that with a few lines of code, understanding the power of Reflection and a bit of knowledge of how the ASP.net rendering system works it is actually quite easy to modernize ListControl-based ListItems and make them play nicely with jQuery's data API.