Search code examples
c#asp.netdata-bindingtemplatefield

How to write a custom templatefield-like DataControlField


I am using a GridView to display data where one of the data columns has type DateTimeOffset. In order to display dates & times in the user's timezone, I save the user's timezone preference to his or her profile (property value key "TimezoneOffset"), and need to access it when formatting dates & times.

If I were to use templatefield, then I would need to write:

<abbr class="datetimeoffset">
<%#
    ((DateTimeOffset)Eval("CreatedDate"))
    .ToOffset(new TimeSpan(-((Int32)Profile.GetPropertyValue("TimezoneOffset"))
                            .ToRepresentativeInRange(-12, 24), 0, 0)).ToString("f") %>
</abbr>

which is too complicated and not reusable.

I tried adding a TimeSpan property to the code-behind (to at least move that out of the data binding expression), but apparently properties of the view's code-behind are inaccessible within <%# ... %>.

Therefore, I think that I need to write a custom DataControlField to format dates & times in the user's timezone.

I have started with:

public class DateTimeOffsetField : DataControlField
{
    private TimeSpan userOffsetTimeSpan;

    protected override DataControlField CreateField()
    {
        return new DateTimeOffsetField();
    }

    protected override void CopyProperties(DataControlField newField)
    {
        base.CopyProperties(newField);
        ((DateTimeOffsetField)newField).userOffsetTimeSpan = userOffsetTimeSpan;
    }

    public override bool Initialize(bool sortingEnabled, System.Web.UI.Control control)
    {
        bool ret = base.Initialize(sortingEnabled, control);
        int timezoneOffset = ((Int32)HttpContext.Current.Profile.GetPropertyValue("TimezoneOffset")).ToRepresentativeInRange(-12, 24);
        userOffsetTimeSpan = new TimeSpan(-timezoneOffset, 0, 0);
        return ret;
    }
}

But now I am stuck. How do I output the HTML <abbr class="datetimeoffset"><%# ((DateTimeOffset)Eval("CreatedDate")).ToOffset(userOffsetTimeSpan).ToString("f") %></abbr> for each cell?

EDIT: I have been reading an article titled Cutting Edge: Custom Data Control Fields. So far I have added:

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

        if (cellType == DataControlCellType.DataCell)
        {
            InitializeDataCell(cell, rowState, rowIndex);
        }
    }

    protected virtual void InitializeDataCell(DataControlFieldCell cell, DataControlRowState rowState, int rowIndex)
    {
        System.Web.UI.Control control = cell;

        if (control != null && Visible)
        {
            control.DataBinding += new EventHandler(OnBindingField);
        }
    }

    protected virtual void OnBindingField(object sender, EventArgs e)
    {
        var target = (System.Web.UI.Control)sender;

        if (target is TableCell)
        {
            TableCell tc = (TableCell)target;
        }
    }

but whereas the article sets the Text property of the TableCell instance, I would like to render a partial view into the table cell. Is that possible?


Solution

  • I figured it out. Here is what I ended up with:

    // DateTimeOffsetField.cs
    public class DateTimeOffsetField : BoundField
    {
        private TimeSpan userOffsetTimeSpan;
    
        protected override DataControlField CreateField()
        {
            return new DateTimeOffsetField();
        }
    
        protected override void CopyProperties(DataControlField newField)
        {
            base.CopyProperties(newField);
            ((DateTimeOffsetField)newField).userOffsetTimeSpan = userOffsetTimeSpan;
        }
    
        public override bool Initialize(bool sortingEnabled, System.Web.UI.Control control)
        {
            bool ret = base.Initialize(sortingEnabled, control);
            int timezoneOffset = ((Int32)HttpContext.Current.Profile.GetPropertyValue("TimezoneOffset")).ToRepresentativeInRange(-12, 24);
            userOffsetTimeSpan = new TimeSpan(-timezoneOffset, 0, 0);
            return ret;
        }
    
        protected override void OnDataBindField(object sender, EventArgs e)
        {
            base.OnDataBindField(sender, e);
    
            var target = (Control)sender;
    
            if (target is TableCell)
            {
                var tc = (TableCell)target;
                var dataItem = DataBinder.GetDataItem(target.NamingContainer);
                var dateTimeOffset = (DateTimeOffset)DataBinder.GetPropertyValue(dataItem, DataField);
                tc.Controls.Add(new TimeagoDateTimeOffset { DateTimeOffset = dateTimeOffset.ToOffset(userOffsetTimeSpan) });
            }
        }
    }
    

    TimeagoDateTimeOffset.cs:

    [DefaultProperty("DateTimeOffset")]
    [ToolboxData("<{0}:TimeagoDateTimeOffset runat=server></{0}:TimeagoDateTimeOffset>")]
    public class TimeagoDateTimeOffset : WebControl
    {
        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("")]
        [Localizable(true)]
        public DateTimeOffset DateTimeOffset
        {
            get { return (DateTimeOffset)ViewState["DateTimeOffset"]; }
            set { ViewState["DateTimeOffset"] = value; }
        }
    
        protected override void RenderContents(HtmlTextWriter writer)
        {
            writer.BeginRender();
            writer.AddAttribute(HtmlTextWriterAttribute.Class, "timeago", false);
            writer.AddAttribute(HtmlTextWriterAttribute.Title, DateTimeOffset.ToString("o"));
            writer.RenderBeginTag("abbr");
            writer.Write(DateTimeOffset.ToString("d"));
            writer.RenderEndTag();
            writer.EndRender();
        }
    }