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.


            
            
Published Sunday, July 13, 2008 12:45 AM by zc0000

Comments

# re: Enhence BulletedList : make the DisplayMode.HyperLink practicable in the case of data-binding

Saturday, August 09, 2008 12:22 PM by Sandeep

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

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

Thursday, February 12, 2009 8:25 AM by dave_winchester

Great post. Is it possible to get a VB version or a downloadable example. Thanks

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

Monday, March 09, 2009 5:01 PM by zc0000

sorry i am unfamiliar with VB

# CH13: LINQ and the bulleted list

Wednesday, January 27, 2010 7:11 AM by CH13: LINQ and the bulleted list

Pingback from  CH13: LINQ and the bulleted list

Leave a Comment

(required) 
(required) 
(optional)
(required)