Pergunta

I've implemented a simple control which renders an abbr tag in order to use the jQuery TimeAgo plugin.

public class TimeAgoControl : System.Web.UI.WebControls.WebControl
{
    [Bindable(true)]
    [DefaultValue(null)]
    public string Iso8601Timestamp
    {
        get { return (string)ViewState["Iso8601Timestamp"]; }
        set { ViewState["Iso8601Timestamp"] = value; }
    }

    [Bindable(true)]
    [DefaultValue(null)]
    public override string ToolTip
    {
        get { return (string)ViewState["ToolTip"]; }
        set { ViewState["ToolTip"] = value; }
    }

    public override void RenderControl(HtmlTextWriter writer)
    {
        writer.AddAttribute("class", "timeago");
        writer.AddAttribute("title", Iso8601Timestamp);
        writer.RenderBeginTag("abbr");
        RenderContents(writer);
        writer.RenderEndTag();
    }

    protected override void RenderContents(HtmlTextWriter writer)
    {
        writer.WriteEncodedText(ToolTip);
    }
}

This works fine. I can use the above control in a GridView TemplateField and that also works fine:

<asp:TemplateField>
    <ItemTemplate>
        <my:TimeAgoControl runat="server" Iso8601Timestamp='<%# Item.LastImportDate.ToString("s") %>' 
                           ToolTip='<%# Item.LastImportDate.ToString("f") %>' />
    </ItemTemplate>
</asp:TemplateField>

Naturally, I'd rather not have to type all of the above every time I want to use the control in a GridView, so I thought I'd abstract the above logic into a custom BoundField where I simply pass the name of the DataField containing the DateTime value to be rendered. Simple enough, extend from System.Web.UI.WebControls.BoundField and override the InitializeDataCell method, add a control to the DataControlFieldCell therein, and attach an handler to the DataBinding event. It works and generates the correct markup:

<td><abbr class="timeago" title="Wednesday, January 29, 2014 16:17">about 23 hours ago</abbr></td>

But when the page does a PostBack, the Control is gone and all I'm left with is

<td>29.01.2014 16:17:17</td>

Please note that the page uses no Ajax features: it contains no UpdatePanels nor ScriptManagers.

I've researched quite a bit, finding this unanswered question as well as this other question which states that one must override the ExtractValuesFromCell method, which in my case is never called. Here's my implementation

public class TimeAgoBoundField : System.Web.UI.WebControls.BoundField
{
    protected override void InitializeDataCell(System.Web.UI.WebControls.DataControlFieldCell cell, System.Web.UI.WebControls.DataControlRowState rowState)
    {
        base.InitializeDataCell(cell, rowState);

        cell.Controls.Add(new TimeAgoControl());

        cell.DataBinding += (sender, e) =>
        {
            var c = (DataControlFieldCell)sender;
            //how do I get the TimeAgoControl within this scope? c.Controls.Count is 0
            var dateTimeValue = (DateTime?)DataBinder.GetPropertyValue(DataBinder.GetDataItem(c.NamingContainer), this.DataField);
            c.Controls.Add(new TimeAgoControl
            {
                Iso8601Timestamp = dateTimeValue.HasValue ? dateTimeValue.Value.ToLocalTime().ToString("s") : this.NullDisplayText,
                ToolTip = DateTimeValue.HasValue ? dateTimeValue.Value.ToLocalTime().ToString("f") : this.NullDisplayText
            });                
        };
    }
}

Note that if I add the control not in the DataBind event handler but rather in InitializeDataCell itself as suggested here, the Controls collection is empty (as a consequence, giving the control an ID and attempting to use FindControl fails returning null). Obviously the DataBind event is not called on postback, but given that ViewState is active at this stage of the lifecycle, I would have expected the control to be persisted on postback.

Any pointers would be greatly appreciated. Thanks in advance.

Foi útil?

Solução

A bit overdue, but in case this helps someone, I solved this by firing up a decompiler (DotPeek) and analyzing what the base control was doing when calling InitializeDataCell. It turns out, this method is protected and is rather called in turn from the public method InitializeCell after some initialization. Additionally, it is responsible for subscribing the OnDataBindField method to the DataBinding event. Further inspection into OnDataBindField revealed that this method is responsible setting the Text property of the TableCell, hence why, after postback, all I was left with was a string representation of the date.

I changed the Initialize method to InitializeCell and overrode the OnDataBindField (without calling its base counterpart) as follows:

public class TimeAgoBoundField : System.Web.UI.WebControls.BoundField
{
    public override bool ReadOnly
    {
        get { return true; }
    }

    public override void InitializeCell(DataControlFieldCell cell, DataControlCellType cellType, DataControlRowState rowState, int rowIndex)
    {
        base.InitializeCell(cell, cellType, rowState, rowIndex);

        if (cellType == DataControlCellType.DataCell)
        {
            cell.Controls.Add(new TimeAgoControl());
        }
    }

    protected override void OnDataBindField(object sender, EventArgs e)
    {
        if (sender is TableCell)
        {
            var cell = (TableCell)sender;
            var cellValue = this.GetValue(cell.NamingContainer);

            if (cellValue != null)
            {
                var timeAgoControl = (TimeAgoControl) cell.Controls[0];
                var dateTimeValue = (DateTime) cellValue;
                var utcDateTime = dateTimeValue.Kind != DateTimeKind.Utc ? dateTimeValue.ToUniversalTime() : dateTimeValue;
                timeAgoControl.ISO8601Timestamp = utcDateTime.ToString("s") + "Z";
            }
            else if (this.NullDisplayText != null)
            {
                cell.Text = this.NullDisplayText;
            }
        }
    }
}

The msdn documentation for BoundField.OnDataBindField now contains this note:

Notes to Inheritors When extending the BoundField class, you can override this method to perform a custom binding routine.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top