Tuesday, June 23, 2009

ASP.NET Label with DropDownList functions to reference another data source

I often encounter the following situation in ASP.NET when using a DetailsView to display a row that contains a reference to a table

<asp:DetailsView ID="detailsView" runat="server" AutoGenerateRows="False"

    DataSourceID="dataSource" DataKeyNames="Id">

    <Fields>

        <asp:TemplateField HeaderText="Name" SortExpression="Id">

            <EditItemTemplate>

                <asp:DropDownList ID="dropDownList" runat="server"

                    DataSourceID="referenceDataSource" DataTextField="Name" DataValueField="Id"

                    SelectedValue='<%# Bind("Id") %>' />

            </EditItemTemplate>

            <ItemTemplate>

                <asp:Label ID="label" runat="server" Text='<%# Eval(" What do I bind to here? ") %>' />

            </ItemTemplate>

        </asp:TemplateField>

    </Fields>

</asp:DetailsView>

I need the DetailsView to display in 3 modes, namely Insert, Edit and ReadOnly. For Insert and Edit modes, a DropDownList takes care of getting the values from the referenceDataSource. However, in ReadOnly mode, I do not want to introduce the DropDownList, for it would have unnecessarily bloat the ViewState and post-back values.

A label should be all that is needed to display, but what do I type to data bind the label to the referenceDataSource? Most of the time, I end up casting the Eval to a DataRowView, getting the Row, and calling GetParentRow. However, that is not a general solution that can be applied everywhere. By changing the data source to use DataReader instead of DataSet, the code in the label will break.

So I decided to roll out my own server control. Taking reference from Creating a Databound Label Control, I created a label control that takes similar attributes as DropDownList, so that all that is needed is to copy the DropDownList from the EditItemTemplate, paste it in ItemTemplate, and changing the tag to ReferenceLabel.

<asp:DetailsView ID="detailsView" runat="server" AutoGenerateRows="False"

    DataSourceID="dataSource" DataKeyNames="Id">

    <Fields>

        <asp:TemplateField HeaderText="Name" SortExpression="Id">

            <EditItemTemplate>

                <asp:DropDownList ID="dropDownList" runat="server"

                    DataSourceID="referenceDataSource" DataTextField="Name" DataValueField="Id"

                    SelectedValue='<%# Bind("Id") %>' />

            </EditItemTemplate>

            <ItemTemplate>

                <huan:ReferenceLabel ID="label" runat="server"

                    DataSourceID="referenceDataSource" DataTextField="Name" DataValueField="Id"

                    SelectedValue='<%# Bind("Id") %>' />

            </ItemTemplate>

        </asp:TemplateField>

    </Fields>

</asp:DetailsView>

Neat huh? Below is the code for the ReferenceLabel. Place it in App_Code and add the following into Web.config.

<configuration>

  <system.web>

    <pages>

      <controls>

        <add tagPrefix="huan" namespace="Huan.Web.UI.WebControls" />

      </controls>

    </pages>

  </system.web>

</configuration>

//-----------------------------------------------------------------------

// <copyright file="ReferenceLabel.cs" company="Jeow Li Huan">

// Copyright (c) Jeow Li Huan. All rights reserved.

// </copyright>

// <remarks>See <see href="http://aspnet.4guysfromrolla.com/articles/081308-1.aspx">Creating a Databound Label Control</see>

// for original implementation.</remarks>

//-----------------------------------------------------------------------

 

namespace Huan.Web.UI.WebControls

{

    using System;

    using System.Collections;

    using System.ComponentModel;

    using System.Web;

    using System.Web.UI;

    using System.Web.UI.WebControls;

 

    /// <summary>

    /// Represents a label control, which displays text referenced from another data source, on a Web page.

    /// </summary>

    [DataBindingHandler("System.Web.UI.Design.WebControls.ListControlDataBindingHandler, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]

    [ParseChildren(false)]

    [ToolboxData("<{0}:ReferenceLabel runat=\"server\" />")]

    public class ReferenceLabel : DataBoundControl

    {

        /// <summary>

        /// Gets or sets the field of the data source that provides the text content of the list items.

        /// </summary>

        /// <value>

        /// A <see cref="T:System.String"/> that specifies the field of the data source that provides the text content of the list items. The default is <see cref="F:System.String.Empty"/>.

        /// </value>

        [Category("Data")]

        [DefaultValue("")]

        [Description("The field in the data source that provides the text.")]

        [Themeable(false)]

        public virtual string DataTextField

        {

            get

            {

                return (string)this.ViewState["DataTextField"] ?? string.Empty;

            }

 

            set

            {

                this.ViewState["DataTextField"] = value;

                this.OnDataPropertyChanged();

            }

        }

 

        /// <summary>

        /// Gets or sets the formatting string used to control how data bound to the label control is displayed.

        /// </summary>

        /// <value>

        /// The formatting string for data bound to the control. The default value is <see cref="F:System.String.Empty"/>.

        /// </value>

        [Category("Data")]

        [DefaultValue("")]

        [Description("The formatting applied to the text. For example, {0:d}.")]

        [Themeable(false)]

        public virtual string DataTextFormatString

        {

            get

            {

                return (string)this.ViewState["DataTextFormatString"] ?? string.Empty;

            }

 

            set

            {

                this.ViewState["DataTextFormatString"] = value;

                this.OnDataPropertyChanged();

            }

        }

 

        /// <summary>

        /// Gets or sets the field of the data source that provides the value of each

        /// list item.

        /// </summary>

        /// <value>

        /// A <see cref="T:System.String"/> that specifies the field of the data source that provides

        /// the value of each list item. The default is <see cref="System.String.Empty"/>.</value>

        [Category("Data")]

        [DefaultValue("")]

        [Description("The field in the data source which provides the item value.")]

        [Themeable(false)]

        public string DataValueField

        {

            get { return (string)this.ViewState["DataValueField"] ?? string.Empty; }

            set { this.ViewState["DataValueField"] = value; }

        }

 

        /// <summary>

        /// Gets or sets the value of the selected item in the list control.

        /// </summary>

        /// <value>

        /// The value of the selected item in the label control. The default is an empty string ("").

        /// </value>

        [Bindable(true, BindingDirection.TwoWay)]

        [Browsable(false)]

        [DefaultValue("")]

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

        [Themeable(false)]

        public object SelectedValue

        {

            get { return this.ViewState["SelectedValue"]; }

            set { this.ViewState["SelectedValue"] = value; }

        }

 

        /// <summary>

        /// Gets or sets the identifier for a server control that the <see cref="T:Huan.Web.UI.WebControls.ReferenceLabel"/> control is associated with.

        /// </summary>

        /// <value>

        /// A string value corresponding to the <see cref="P:System.Web.UI.Control.ID"/> for a server control contained in the Web form. The default is an empty string (""), indicating that the <see cref="T:Huan.Web.UI.WebControls.ReferenceLabel"/> control is not associated with another server control.

        /// </value>

        [Category("Accessibility")]

        [DefaultValue("")]

        [Description("The ID of the control associated with the Label.")]

        [IDReferenceProperty]

        [Themeable(false), TypeConverter(typeof(AssociatedControlConverter))]

        public virtual string AssociatedControlID

        {

            get { return (string)this.ViewState["AssociatedControlID"] ?? string.Empty; }

            set { this.ViewState["AssociatedControlID"] = value; }

        }

 

        /// <summary>

        /// Gets the text content of the label.

        /// </summary>

        /// <value>

        /// The text content of the control. The default value is <see cref="System.String.Empty"/>.

        /// </value>

        [Category("Appearance")]

        [DefaultValue("")]

        [Description("The text to be shown for the label.")]

        [Localizable(true)]

        public virtual string Text

        {

            get { return (string)this.ViewState["Text"] ?? string.Empty; }

        }

 

        /// <summary>

        /// Gets the HTML tag that is used to render the label.

        /// </summary>

        /// <value>

        /// The <see cref="T:System.Web.UI.HtmlTextWriterTag"/> value used to render the label.

        /// </value>

        protected override HtmlTextWriterTag TagKey

        {

            get

            {

                if (this.AssociatedControlID.Length != 0)

                {

                    return HtmlTextWriterTag.Label;

                }

 

                return base.TagKey;

            }

        }

 

        /// <summary>

        /// When overridden in a derived class, binds data from the data source to the control.

        /// </summary>

        /// <param name="data">The <see cref="T:System.Collections.IEnumerable"/> list of data returned from a <see cref="M:System.Web.UI.WebControls.DataBoundControl.PerformSelect"/> method call.</param>

        /// <exception cref="T:System.ArgumentOutOfRangeException">

        /// The selected value is not in the list of avaliable values.

        /// </exception>

        protected override void PerformDataBinding(IEnumerable data)

        {

            base.PerformDataBinding(data);

 

            if (this.DesignMode)

            {

                this.SetText(string.IsNullOrEmpty(this.ID) ? "abc" : this.ID);

                return;

            }

 

            if (data != null && this.SelectedValue != null)

            {

                // Clear out the Text property

                this.ClearText();

 

                string formatString = this.DataTextFormatString.Length == 0 ? "{0}" : this.DataTextFormatString;

 

                // Get the DataTextFormatString field value for the FIRST record

                foreach (object obj in data)

                {

                    if (this.DesignMode)

                    {

                        this.SetText(string.Format(formatString, "Databound"));

                        return;

                    }

                    else

                    {

                        bool hasDataValueField = this.DataValueField.Length != 0;

                        object dataValue = hasDataValueField ? DataBinder.GetPropertyValue(obj, this.DataValueField) : obj;

                        if (this.SelectedValue.Equals(dataValue))

                        {

                            bool hasDataTextField = this.DataTextField.Length != 0;

                            if (hasDataTextField)

                                this.SetText(DataBinder.GetPropertyValue(obj, this.DataTextField, formatString));

                            else if (hasDataValueField)

                                this.SetText(DataBinder.GetPropertyValue(obj, this.DataValueField, formatString));

                            else

                                this.SetText(string.Format(formatString, obj.ToString()));

 

                            return;

                        }

                    }

                }

 

                throw new ArgumentOutOfRangeException("SelectedValue", string.Format("'{0}' has a SelectedValue which is invalid because it does not exist in the list of items.", this.ID));

            }

        }

 

        /// <summary>

        /// Adds HTML attributes and styles that need to be rendered to the specified <see cref="T:System.Web.UI.HtmlTextWriter"/> object.

        /// </summary>

        /// <param name="writer">An <see cref="T:System.Web.UI.HtmlTextWriter"/> that represents the output stream that renders HTML contents to the client.</param>

        protected override void AddAttributesToRender(HtmlTextWriter writer)

        {

            if (this.AssociatedControlID.Length != 0)

            {

                Control control = this.FindControl(this.AssociatedControlID);

                if (control == null && !this.DesignMode)

                    throw new HttpException(string.Format("The ReferenceLabel '{0}' cannot find associated control ID '{1}'.", this.ID, this.AssociatedControlID));

                else

                    writer.AddAttribute(HtmlTextWriterAttribute.For, control.ClientID);

            }

 

            base.AddAttributesToRender(writer);

        }

 

        /// <summary>

        /// Renders the items in the <see cref="T:System.Web.UI.WebControls.ListControl"/> control.

        /// </summary>

        /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> that represents the output stream used to write content to a Web page.</param>

        protected override void RenderContents(HtmlTextWriter writer)

        {

            HttpUtility.HtmlEncode(this.Text, writer);

        }

 

        /// <summary>

        /// Sets the text in the view state.

        /// </summary>

        /// <param name="text">The text to be stored in the view state.</param>

        protected virtual void SetText(string text)

        {

            this.ViewState["Text"] = text;

        }

 

        /// <summary>

        /// Clears the text stored in the view state.

        /// </summary>

        protected virtual void ClearText()

        {

            this.ViewState.Remove("Text");

        }

    }

}

Limitations:

Due to the aim of reducing ViewState, only the initial selected text is stored in the ViewState. Changing the SelectedValue after the control has been data bound will not change the displayed text.

No comments: