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.