Create a custom control in Sitecore: DateTime with TimeZone

DateTimeWithTimeZone

2015 will be a great year, especially when Sitecore recently released their flagship Sitecore 8. I just love the design and all the new cool things that comes with it.

I want to show you guys how to make a custom control – DateTime with TimeZone. There is a great post out there which explains it very well – Sitecore Date Time picker with time zone. In this case I needed to store the DateTime together with the TimeZone.
In “raw values” the control will look like this:
DateTimeWithTimeZoneRaw

What we need to do:
1. Create the control(Content editor)
2. Register the control in Sitecore(Core database)
3. Create a render field pipeline(For page editor and the “website”)

Let us dive in to the code πŸ™‚

Create the control

First we do the actual control which will have a DateTime picker and a dropdown containing all TimeZones. We need to create a class which inherits from Sitecore.Shell.Applications.ContentEditor.DateTime(Sitecore DateTime picker). In the DoRender method we will add/create the dropdown with timezones.

protected override void DoRender(System.Web.UI.HtmlTextWriter output)
{

    if (!string.IsNullOrWhiteSpace(base.RealValue) && base.RealValue.Contains("|"))
    {
        TimeZone = _dateWithTimeZoneService.GetTimeZoneValue(base.RealValue);
        base.SetValue(_dateWithTimeZoneService.CalculateDateTimeWithTimeZone(base.RealValue));
    }
                 
    output.Write("<div style='display:inline;float:left'>");
    base.DoRender(output);
    output.Write(TimeZoneDroplist());
    output.Write("</div>");

}

Since we store the DateTime value together with the TimeZone id (base.RealValue property from class Sitecore.Shell.Applications.ContentEditor.Date holds the “raw value”) we need to trick the Sitecore DateTime Picker (It expects a DateTime value). That’s why we set a calculated DateTime value in the base.SetValue method (From Sitecore.Shell.Applications.ContentEditor.Date)

The method for generating the dropdown with timezones is quite straight forward. To get the timezones we use the System.TimeZoneInfo.GetSystemTimeZones() method.

private string TimeZoneDroplist()
{
    StringBuilder stringBuilderSelect = new StringBuilder();

    stringBuilderSelect.AppendFormat(@"<select {0} {1} >", this.GetControlAttributes(), string.IsNullOrWhiteSpace(base.RealValue) ? "disabled" : string.Empty);
    stringBuilderSelect.AppendLine();

    stringBuilderSelect.AppendFormat(@"<option value='' >{0}</option>", "Please select a time zone");
           
    foreach (TimeZoneInfo timeZoneInfo in TimeZoneInfo.GetSystemTimeZones())
    {
        stringBuilderSelect.AppendFormat(@"<option value='{0}' {1} >{2}</option>", timeZoneInfo.Id, TimeZone == timeZoneInfo.Id ? "selected" : string.Empty, timeZoneInfo.DisplayName);
        stringBuilderSelect.AppendLine();
    }

    stringBuilderSelect.AppendLine();
    stringBuilderSelect.Append("</select>");

    return stringBuilderSelect.ToString();
}

The TimeZone property(viewstate) holds the selected TimeZone Id.

To store the the combined data, DateTime value and TimeZone id, we need to set the RealValue property (From Sitecore.Shell.Applications.ContentEditor.Date). We will do this in the LoadPostData method.

protected override bool LoadPostData(string value)
{
    if (value == null)
        return false;

    if (!base.RealValue.Contains(value))
        base.RealValue = string.Format("{0}|{1}", _dateWithTimeZoneService.GetDateTimeValue(base.RealValue), value);
           
    return true;
}

The “value parameter” will contain the selected TimeZone id from the TimeZone dropdown.

Here is the full code for the control.

public class DateTimePickerWithTimeZone : Sitecore.Shell.Applications.ContentEditor.DateTime
{

    private readonly IDateWithTimeZoneService _dateWithTimeZoneService;

    public string Format
    {
        get { return base.GetViewStateString("Value.Format"); }
        set
        {
            base.SetViewStateString("Value.Format", value);
        }
    }

    public string TimeZone
    {
        get { return base.GetViewStateString("Value.TimeZone"); }
        set
        {
            base.SetViewStateString("Value.TimeZone", value);
        }
    }

    public DateTimePickerWithTimeZone(): base()
    {
        _dateWithTimeZoneService = new DateWithTimeZoneService();
        Format = "MM/dd/yyyy";
    }

    protected override void DoRender(System.Web.UI.HtmlTextWriter output)
    {

        if (!string.IsNullOrWhiteSpace(RealValue) && base.RealValue.Contains("|"))
        {
            TimeZone = _dateWithTimeZoneService.GetTimeZoneValue(RealValue);
            SetValue(_dateWithTimeZoneService.CalculateDateTimeWithTimeZone(RealValue));
        }
                 
        output.Write("<div style='display:inline;float:left'>");
        base.DoRender(output);
        output.Write(TimeZoneDroplist());
        output.Write("</div>");

    }

    private string TimeZoneDroplist()
    {
        StringBuilder stringBuilderSelect = new StringBuilder();

        stringBuilderSelect.AppendFormat(@"<select {0} {1} >", this.GetControlAttributes(), string.IsNullOrWhiteSpace(RealValue) ? "disabled" : string.Empty);
        stringBuilderSelect.AppendLine();

        stringBuilderSelect.AppendFormat(@"<option value='' >{0}</option>", "Please select a time zone");
           
        foreach (TimeZoneInfo timeZoneInfo in TimeZoneInfo.GetSystemTimeZones())
        {
            stringBuilderSelect.AppendFormat(@"<option value='{0}' {1} >{2}</option>", timeZoneInfo.Id, TimeZone == timeZoneInfo.Id ? "selected" : string.Empty, timeZoneInfo.DisplayName);
            stringBuilderSelect.AppendLine();
        }

        stringBuilderSelect.AppendLine();
        stringBuilderSelect.Append("</select>");

        return stringBuilderSelect.ToString();
    }

    protected override bool LoadPostData(string value)
    {
        if (value == null)
            return false;

        if (!base.RealValue.Contains(value))
            base.RealValue = string.Format("{0}|{1}", _dateWithTimeZoneService.GetDateTimeValue(base.RealValue), value);
           
        return true;
    }
       
}

For calculating DateTime with Timezone and refining data we need a service/helper class(It will also be used by the render field class)

public interface IDateWithTimeZoneService
{
    string CalculateDateTimeWithTimeZone(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe);

    string GetDateTimeValue(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe);

    string GetTimeZoneValue(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe);
}

public class DateWithTimeZoneService : IDateWithTimeZoneService
{

    /// <summary>
    /// Method for calculating datetime with timezone
    /// </summary>
    /// <param name="combinedDateTimeAndTimeZoneValueSeperatedWithPipe"></param>
    /// <returns></returns>
    public string CalculateDateTimeWithTimeZone(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe)
    {

        String dateTime = GetDateTimeValue(combinedDateTimeAndTimeZoneValueSeperatedWithPipe);
        string zoneId = GetTimeZoneValue(combinedDateTimeAndTimeZoneValueSeperatedWithPipe);

        Assert.IsTrue(DateUtil.IsIsoDate(dateTime), "Not valid date");

        Assert.IsNotNull(zoneId, "TimeZone id is missing");

        DateTime currentDateTimeUtc = DateUtil.IsoDateToDateTime(dateTime).ToUniversalTime();
        TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(zoneId);
        DateTime localDateTime = TimeZoneInfo.ConvertTimeFromUtc(currentDateTimeUtc, zone);

        return DateUtil.ToIsoDate(localDateTime);
    }

    /// <summary>
    /// Method to get datetime
    /// </summary>
    /// <param name="combinedDateTimeAndTimeZoneValueSeperatedWithPipe"></param>
    /// <returns></returns>
    public string GetDateTimeValue(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe)
    {
        return combinedDateTimeAndTimeZoneValueSeperatedWithPipe.Contains("|") ? combinedDateTimeAndTimeZoneValueSeperatedWithPipe.Substring(0, combinedDateTimeAndTimeZoneValueSeperatedWithPipe.IndexOf("|", System.StringComparison.Ordinal)) : combinedDateTimeAndTimeZoneValueSeperatedWithPipe;
    }

    /// <summary>
    /// Method to get timezone id
    /// </summary>
    /// <param name="combinedDateTimeAndTimeZoneValueSeperatedWithPipe"></param>
    /// <returns></returns>
    public string GetTimeZoneValue(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe)
    {
        return combinedDateTimeAndTimeZoneValueSeperatedWithPipe.Contains("|") ? combinedDateTimeAndTimeZoneValueSeperatedWithPipe.Substring(combinedDateTimeAndTimeZoneValueSeperatedWithPipe.IndexOf("|", System.StringComparison.Ordinal) + 1) : string.Empty;
    }
}

Register the control in Sitecore

Next thing to do is to register the control in Sitecore. We will do this in System/Field types in the Core database. The easiest way is to copy the DateTime item in System Types – /sitecore/system/Field types/Simple Types/Datetime. Give it a good name and remove the “WebEdit Buttons” folder.
registercontrol
Enter the assembly name and the class for the control.

To use it just create a template and select our new field type.
SitecoreTemplate

Create a render field pipeline

OK so now we have a nice control which will work perfect in the content editor but not in the page editor or on the actual “website”. Since we store both the DateTime value and the Timezone id in a field it will mess up the render field pipeline. To fix it we need to make our own “render field pipeline” and it will be very similar to Sitecore’s Sitecore.Pipelines.RenderField.GetDateFieldValue. So we just duplicate the class and give it a good name – GetDateTimeWithTimeZoneValue

public class GetDateTimeWithTimeZoneValue
{

    private readonly IDateWithTimeZoneService _dateWithTimeZoneService;

    public GetDateTimeWithTimeZoneValue()
    {
        _dateWithTimeZoneService = new DateWithTimeZoneService();
    }

    /// <summary>
    /// Runs the processor.
    /// 
    /// </summary>
    /// <param name="args">The arguments.</param>
    public void Process(RenderFieldArgs args)
    {
        string fieldTypeKey = args.FieldTypeKey;
            
        if (fieldTypeKey != "datetimewithtimezone")
            return;

        DateRenderer renderer = this.CreateRenderer();
        renderer.Item = args.Item;
        renderer.FieldName = args.FieldName;
        renderer.FieldValue = _dateWithTimeZoneService.CalculateDateTimeWithTimeZone(args.FieldValue);

        renderer.Parameters = args.Parameters;
            
        if (!string.IsNullOrEmpty(args.Parameters["format"]))
            args.WebEditParameters["format"] = args.Parameters["format"];
            
        RenderFieldResult renderFieldResult = renderer.Render();
        args.Result.FirstPart = renderFieldResult.FirstPart;
        args.Result.LastPart = renderFieldResult.LastPart;
    }

    /// <summary>
    /// Creates the renderer.
    /// 
    /// </summary>
    /// 
    /// <returns>
    /// The renderer.
    /// </returns>
    protected virtual DateRenderer CreateRenderer()
    {
        return new DateRenderer();
    }

}

We will set the renderer.FieldValue by calculating the correct DateTime by using the combined value(DateTime and TimeZoneId) in args.FieldValue.

In order to make the pipeline work we need to do a patch config file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="Sandbox.CustomFields.Pipelines.RenderField.GetDateTimeWithTimeZoneValue, Sandbox.CustomFields"
                    patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetDateFieldValue, Sitecore.Kernel']">
        </processor>
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

That’s all for now folks πŸ™‚

Happy New Year!


5 thoughts on “Create a custom control in Sitecore: DateTime with TimeZone

  1. Hi,

    Great article! I found a bug, i think. When i save datea time and zone, i open item again and change just time and save. I get zone deleted and i need to select it again and save. Is this happening only for me , or did someone else notice this.

    Thanks

    Like

    1. Well spotted Uros, you found a bug!
      I will have a look at it, unfortunately I’m a bit busy for the moment. Feel free to dive into it, please let me know if you find a solution.
      Thanks for reading my blog πŸ™‚

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.