Binding Multiple Fields to ASP.NET ListControl classes

Governor Technology is a London-based software consultancy, specialising in ASP.NET & Silverlight development. Our clients include Microsoft, Thomson Reuters & Citigroup. We've been a Microsoft Certified Partner since 2004 and a Silverlight Partner since 2008.

Introduction

The ASP.NET RadioButtonList is a databound control which displays items as a mutually exclusive set of options in a Web Form. The data which is bound to the label part of each option can be formatted using a format string, but only a single field can be bound to the label.

This article shows an implementation which allows multiple fields to be bound to the item labels. We needed this in order to enrich the display of radio button lists in one of our applications. We’re going to look in detail at the code for the existing ASP.Net controls, which you can browse by disassembling the System.Web assembly in Reflector. The kind of solution presented here should also work equally well for CheckBoxList and DropDownList, because all three controls inherit most of their data binding functionality from the ListControl base class.

GridView Hyperlink Field

Anyone who has used GridView in any detail will know that you can create a type of databound column in the GridView which renders out a Hyperlink. The underlying data which is used to generate the text and URL of the hyperlink, and the format String to produce the actual output, are specified in properties declared in the markup.

Because often multiple fields are required to generate the Url (e.g. if the Url has multiple query string parameters), the DataNavigateUrlFields property accepts a comma delimited list of fields to bind. To display multiple items, the format string simply refers to each item required:

<asp:GridView ID="gvMaster" runat="server" AutoGenerateColumns="False">
    <Columns>
        <asp:HyperLinkField DataNavigateUrlFields="CategoryID,Processed" 
        Text="Link To MyPage" 
        DataNavigateUrlFormatString="~/MyPage.aspx?Category={0}&ShowProcessed={1}" />
    </Columns>
</asp:GridView>

Inspecting this class in Reflector show that this is automatically converted and stored as a string array:

[TypeConverter(typeof(StringArrayConverter))]
[DefaultValue((string)null)]
public virtual string[] DataTextFields {get; set;}

During data binding, the object properties corresponding to each field are investigated to yield reflection objects:

// Caches the reflection data for databinding
private PropertyDescriptor[] urlFieldDescs;

// called when each hyperlink is databound (edited for clarity)
private void OnDataBindField(object sender, EventArgs e)
{
   if (this.urlFieldDescs == null)
   {
      PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(component);
      string[] dataNavigateUrlFields = this.DataNavigateUrlFields;
      int num = dataNavigateUrlFields.Length;
      this.urlFieldDescs = new PropertyDescriptor[num];
      for (int i = 0; i < num; i++)
      {
          dataTextField = dataNavigateUrlFields[i];
          if (dataTextField.Length != 0)
          {
              this.urlFieldDescs[i] = properties.Find(dataTextField, true);
              if ((this.urlFieldDescs[i] == null) && !base.DesignMode)
              {
                throw new HttpException(SR.GetString("Field_Not_Found", 
                                        new object[] { dataTextField }));
              }
          }
       }
    }
    ...
}

From these property definitions, reflection is used to get an array of values for the current object:

// get an array of values from the current dataItem’s properties
int length = this.urlFieldDescs.Length;
object[] dataUrlValues = new object[length];

for (int j = 0; j < length; j++)
{
     if (this.urlFieldDescs[j] != null)
     {
         dataUrlValues[j] = this.urlFieldDescs[j].GetValue(component);
     }
}
HyperLink link = (HyperLink)sender;   
string s = this.FormatDataNavigateUrlValue(dataUrlValues);
link.NavigateUrl = s;

The actual formatting method just calls String.Format() on the DataFormatString using the array of values:

protected virtual string FormatDataNavigateUrlValue (object value)
{
    string str = string.Empty;
    if (DataBinder.IsNull(value))
    {
        return str;
    }
    string dataTextFormatString = this.DataTextFormatString;
    if (dataTextFormatString.Length == 0)
    {
        return value.ToString();
    }
    return string.Format(CultureInfo.CurrentCulture, 
                        dataTextFormatString, 
                        new object[] { value });
}

RadioButtonList

So much for HyperLinkField – but why can we not do the same in RadioButtonList? The difference is that RadioButtonList derives indirectly from ListControl, which controls the data binding for values and labels using the DataTextField and DataValueField properties. Binding multiple fields to values or labels is not supported in ListControl, and has not been added to RadioButtonList. To add this functionality, we’re going to derive from RadioButtonList and make a few changes.

Firstly, we need to customise the properties of our new class so that multiple text fields can be specified. The existing DataTextField is inherited and is public in scope, so we can’t really hide that. For simplicity’s sake, the best we can do is just complain if it’s accessed. With DataTextField sabotaged, we add a DataTextFields property as per the example above. By specifying the TypeConverter attribute, ASP.Net can parse the property declaration in the aspx page into an array without any intervention on our part:

public class MultiFieldRBList : RadioButtonList
{
    public override string DataTextField
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    /// <summary>
    /// A comma separated list of the fields used in the 
    /// databinding of the text for each ListItem
    /// </summary>
    [TypeConverter(typeof(StringArrayConverter))]
    [DefaultValue((string)null)]
    public virtual string[] DataTextFields
    {
        get
        {
            object currentValues = base.ViewState["DataTextFields"];
            if (currentValues != null)
            {
                return (string[])((string[])currentValues).Clone();
            }
            return new string[0];
        }
        set
        {
            string[] strArray = base.ViewState["DataTextFields"] as string[];
            if (!this.StringArraysEqual(strArray, value))
            {
                if (value != null)
                {
                    base.ViewState["DataTextFields"] = (string[])value.Clone();
                    if (base.Initialized)
                    {
                        base.RequiresDataBinding = true;
                    }
                }
                else
                {
                    base.ViewState["DataTextFields"] = null;
                }
            }
        }
    }

So far, we’re just reading the field list into a property in the class. Now we need to hook into the existing architecture and override PerformDataBinding(). This method is provided by the DataboudControl base class, and is called after data has been selected from the data source:

/// <summary>
/// Overrides the default data binding after the select method has been called.
/// This allows us to create the ListItems using multiple fields
/// </summary>
protected override void PerformDataBinding(IEnumerable dataSource)
{
    /// do our stuff to create new list items 
    ///much like the HyperLinkField solution, so not repeated here-
    ///get the download attached to this article for the full source code...

    base.PerformDataBinding(null);
}

ListControl’s implementation of PerformDataBinding() creates a new set of ListItem objects and adds them to the Items collection. We’re going to replace that with our own implementation, hence the override. There are also some things which ListControl does in this method involving managing the current item selection. This uses private properties which we can’t access from our subclass. This raises the problem of an ugly hack involving copying more code into our subclass, or calling the base class method as well, and then getting the default behaviour adding items as well as the ones we’ve created.

Fortunately, this can be avoided by simply passing in a null instead of the data collection. The reason this works is largely serendipitous, and depends on the fact that if the method in ListControl is called with a null instead of actual data, it does the work we need without clearing down the items. And that’s basically it – include the control in your web application and generate list labels from multiple fields. The following example is in the code download, and takes the names and years of arcade games provided by a data souce:

<GOV:MultiFieldRBList ID="MultiFieldRBList1" DataSourceID="odsGames" 
        runat="server" DataTextFields="Title, Year" CssClass="List"
        DataValueField="Id" DataTextFormatString="<b>{0}</b> ({1})" />

 <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
 TypeName="Game" SelectMethod="GetAll" />

The resulting output being a formatted RadioButtonList with the values.

You might also like...

Comments

Neil Dodson

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.” - Antoine de Saint Exupéry