Attention: We are retiring the ASP.NET Community Blogs. Learn more >

Enhance BulletedList : make the DisplayMode.HyperLink practicable in the case of data-binding

BulletedList , like other peers such as "RadioButtonList" , "DropDownList" , "ListBox" , "CheckBoxList" , inherits ListControl due to their analogous displaying style. However , BulletedList has its unique definition and usage of ListItem collection, especially in the "Value" property , which makes it distinguished from other peers.

First I will explain the similarity of other typical ListControl-based controls than BulletedList. As for "DropDownList" and "ListBox" , which both are even to "select" html element ( "ListBox" denotes a "select" with a "multiple" attribute ), each ListItem in their "Items" collection represents an "option" , coherently making "Value" property reflecting "value" attribute; As for "RadioButtonList" and "CheckBoxList" , each ListItem of their Items indicates an "input" element , ( "type='radio'" for RadioButtonList and "type='checkbox' for CheckBoxList" ) naturally mapping "Value" property to the "value" , too. In summary , these ListControls' "ListItem" simply corresponds to a single html element ( "option" or "input" ) and "Value" property directly maps the attribute with same name.

However , in the case of "BulletedList" , situation becomes a little more complex. Generally , a ListItem denotes a "li" html element , which has not an built-in retrievable "value" ( actually the "value" of "li" has been deprecated for a long time ) and is by no means a single element but a container. ListItem renders inner content depending on different DisplayMode. We may find the details from the concrete implementation of rendering inner text between "<li>" and "</li>":

/// <devdoc> 
        ///    <para>Renders the ListItems as bullets in the bulleted list.</para> 
        /// </devdoc>
        protected internal override void RenderContents(HtmlTextWriter writer) { 
            _cachedIsEnabled = IsEnabled;

            if (_itemCount == -1) {
                for (int i = 0; i < Items.Count; i++) { 
                    Items[i].RenderAttributes(writer);
                    writer.RenderBeginTag(HtmlTextWriterTag.Li); 
                    RenderBulletText(Items[i], i, writer); 
                    writer.RenderEndTag();
                } 
            }
            else {
                for (int i = _firstItem; i < _firstItem + _itemCount; i++) {
                    Items[i].RenderAttributes(writer); 
                    writer.RenderBeginTag(HtmlTextWriterTag.Li);
                    RenderBulletText(Items[i], i, writer); 
                    writer.RenderEndTag(); 
                }
            } 
        }
...

 /// <devdoc>
        ///     <para>Writes the text of each bullet according to the list's display mode.</para> 
        /// </devdoc>
        protected virtual void RenderBulletText(ListItem item, int index, HtmlTextWriter writer)
In Text mode , each ListItem just encapsulates "Text" value in a "span" element , leaving "Value" a forever inaccessible and unhelpful field:
 case BulletedListDisplayMode.Text:
                    if (!item.Enabled) { 
                        writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "disabled");
                        writer.RenderBeginTag(HtmlTextWriterTag.Span);
                    }
                    HttpUtility.HtmlEncode(item.Text, writer); 
                    if (!item.Enabled) {
                        writer.RenderEndTag(); 
                    } 
                    break;

In LinkButton Mode , each ListItem outputs "LinkButton-style" anchor element , making Text and Value separately denoting the "LinkButton" 's "Text" and "CommandArgument" properties. Actually only in this mode may a user trigger some event to obtain the "SelectedValue" ( corresponding LinkButton's CommandArgument ) , thus making BulletedList appearing closet to its sibling control:

   case BulletedListDisplayMode.LinkButton:
                    if (_cachedIsEnabled && item.Enabled) {
                        writer.AddAttribute(HtmlTextWriterAttribute.Href, GetPostBackEventReference(index.ToString(CultureInfo.InvariantCulture)));
                    } 
                    else {
                        writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "disabled"); 
                    } 

                    RenderAccessKey(writer, AccessKey); 
                    writer.RenderBeginTag(HtmlTextWriterTag.A);
                    HttpUtility.HtmlEncode(item.Text, writer);
                    writer.RenderEndTag();
                    break; 

Then let's check the topical mode. In HyperLink mode ,instead of generating a "LinkButton-style" anchor , ListItem just produce a primitive one without javascripts which can sense user's click , making the Value part of url. From following code we may clarify how the "Value" perform as "part" of a sound url: it serves as the parameter of ResolveClientUrl function , which returns a client-recognizable url string.

  case BulletedListDisplayMode.HyperLink:
                    if (_cachedIsEnabled && item.Enabled) {
                        writer.AddAttribute(HtmlTextWriterAttribute.Href, ResolveClientUrl(item.Value));
                        string target = Target; 
                        if (!String.IsNullOrEmpty(target)) {
                            writer.AddAttribute(HtmlTextWriterAttribute.Target, Target); 
                        } 
                    }
                    else { 
                        writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "disabled");
                    }

                    RenderAccessKey(writer, AccessKey); 
                    writer.RenderBeginTag(HtmlTextWriterTag.A);
                    HttpUtility.HtmlEncode(item.Text, writer); 
                    writer.RenderEndTag(); 
                    break;

From the code we also know why a series of url links in the format of "http://CurrentVirtualDirectory/Value" display after data-binding a BulletedList in HyperLink DisplayMode. In mose cases, the original "url" after databinding is nonsense , so I dare say that "HyperLinkMode" is absolutely impracticable when dealing with Data-Binding senario. However, I bet in the beginning most developers expect such a great "BulletedList" can list a series of normalized and formatted url links in HyperLink DisplayMode when binding to a datasource , just through some simple settings; while after a deep study they only find it incapable of achieving this and get disappointed!

One way to solve the problem is attaching OnDataBind event to a handler method, in which we can reset each ListItem's Value with a formatted one according to specific format string. It is likely to become a drab task and hard to manage if such application is frequent in a project.

A more decent and managable solution is building a new ListControl inheriting BulletedList, plusing an entry ( call it "DataValueFormatString" ) to format the "DataValueField" to make "Value"s meaningful and useful when collected through data-binding and applied as url.

[DefaultValue(""),Themeable(false),Category("Data"),
  Description("Data Value Format String For HyperLink Displaying")]
    public virtual string DataValueFormatString
    {
        get
        {
            object s = ViewState["DataValueFormatString"];
            return ((s == null) ? string.Empty : (string)s);
        }
        set
        {
            ViewState["DataValueFormatString"] = value;
            if (Initialized)
            {
                RequiresDataBinding = true;
            }
        }
    }

Then we should adjust the DataBinding logic slightly , ensuring that after each call of data-binding to certain datasource , item's value be immediately formatted via the DataValueFormatString. Such task require us to know "where" and "how" to improve the databinding function code.

A scrutiny into the workaround of DataBoundControl's data-binding mechanism reveals that any DataBoundControl-derived control must override PerformDataBinding method to form its particular data-binding logic.

/// <devdoc> 
///  This method should be overridden by databound controls to perform their databinding.
///  Overriding this method instead of DataBind() will allow the DataBound control developer 
///  to not worry about DataBinding events to be called in the right order. 
/// </devdoc></span>
protected internal virtual void PerformDataBinding(IEnumerable data) { }

Shifting to our case , ListControl is a typical deriviation of DataBoundControl and has its own "PerformDataBinding" implementation . All the children ( including BulletedList ) obey exactly parent's data-binding behavior without any further overriding:

protected internal override void PerformDataBinding(IEnumerable dataSource) {
base.PerformDataBinding(dataSource); 
 
            if (dataSource != null) {
                bool fieldsSpecified = false; 
                bool formatSpecified = false;

                string textField = DataTextField;
                string valueField = DataValueField; 
                string textFormat = DataTextFormatString;
 
                if (!AppendDataBoundItems) { 
                    Items.Clear();
                } 

                ICollection collection = dataSource as ICollection;
                if (collection != null) {
                    Items.Capacity = collection.Count + Items.Count; 
                }
 
                if ((textField.Length != 0) || (valueField.Length != 0)) { 
                    fieldsSpecified = true;
                } 
                if (textFormat.Length != 0) {
                    formatSpecified = true;
                }
 
                foreach (object dataItem in dataSource) {
                    ListItem item = new ListItem(); 
 
                    if (fieldsSpecified) {
                        if (textField.Length > 0) { 
                            item.Text = DataBinder.GetPropertyValue(dataItem, textField, textFormat);
                        }
                        if (valueField.Length > 0) {
                            item.Value = DataBinder.GetPropertyValue(dataItem, valueField, null); 
                        }
                    } 
                    else { 
                        if (formatSpecified) {
                            item.Text = String.Format(CultureInfo.CurrentCulture, textFormat, dataItem); 
                        }
                        else {
                            item.Text = dataItem.ToString();
                        } 
                        item.Value = dataItem.ToString();
                    } 
 
                    Items.Add(item);
                } 
            }

            <span>// try to apply the cached SelectedIndex and SelectedValue now</span>
            ......
 }

From above code we see that "DataBinder" has responsibility to retrieve acutal property value of a concrete data object according to user-assinged binding field and format the value by corresponding format-string if specified.

Commonly , ListControl assumes only "Text" of each Listitem needs formating , so it opens an Property "DataTextFormatString" and format displaying Text with it by invoking GetPropertyValue as follows :

  item.Text = DataBinder.GetPropertyValue(dataItem, textField, textFormat);

However , it has no function to format "Value" via certain unified format string. So it set the formatstring paramter as null , just simply extracting the original property value , assigning to ListItem's Value field.

 item.Value = DataBinder.GetPropertyValue(dataItem, valueField, null);

Since we do not intend to shake the root of ListControl's Binding-behaviour but add a little code to remend the result based on retrieved collection ------ in other words , we need not attach each dataItem enumerated from source to gain additional essential data information for Value resetting , I do not suggest overriding PerformDataBinding. We may put the function in an after appropriate location : OnDataBound , which basically captures the handler and invokes it. when overriding we should reset each Item's "Value" to a formatted one based on "DataValueFormatString" if specified :

  protected override void OnDataBound(EventArgs e)
    {
        // if DataValueFormatString specified we should change each ListItem's Value to formatted style
        // actually such setting has its real value in the case of "HyperLink" DisplayMode
        if (DataValueField.Length > 0 && DataValueFormatString.Length > 0)
            foreach (ListItem item in Items)
                item.Value = string.Format(DataValueFormatString, item.Value);
     
        base.OnDataBound(e);
    }

If such enhenced BulletedList might be universely used in your project , it is a good idea to map built-in BulletedList to the new derived one by modifying the web.config file:

 <tagMapping>
        <add tagType="System.Web.UI.WebControls.BulletedList" mappedTagType="EnhencedBulletedList"/>
    </tagMapping>

Then we go to the last step : how to use it ? It is as easy as you can imagine : just setting the "DataValueFormatString" as you have done many times against the HyperLinkField when handling GridView displaying:

<asp:BulletedList runat="server" ID="newsList" DisplayMode="HyperLink" DataTextField="Subject"
        DataValueField="NewsId" DataValueFormatString="~/News.aspx?NewsId={0}">
    </asp:BulletedList>

OK , after these steps we achive a more powerful version than basic one. Now we can easily and perfectly adopt it in the case of "HyperLinkMode combining Data-Binding" . In fact , such upgrading involves little codes and is apparently not a complex and long story. But I would rather explain it from a detailed and intrinsic perspective thus to supply a comparatively clear vision , which may help you learn & rectify my thought and better my measure. Sample Code tails the text for downloading , including several official source code files for a quick reference.


3 Comments

  • Why isn't this functionality available out of the box?


  • sorry i am unfamiliar with VB

  • Your article described me to a tee "while after a deep study they only find it incapable of achieving this and get disappointed".

    I was about to ditch elegant, typed, code and go with literals but your fine work has helped me keep my code readable and easier to manage.

    Many thanks for your post.

Comments have been disabled for this content.