Wednesday 1 July 2015

Sitecore : Datetime Field with Timezone

Working with Date Time and Timezones is always headache. Recently we faced issue with calendar events generated by our sitecore application in different regions. The start date and end date in generated ICS file was failing to show exact date time specific to end user because of the difference in timezones.

Why that happened?

Curretly there is no mechanism in sitecore to store date and time in different timezones. It simply considers the server timezone - on which the sitecore instance runs - and stores date time accordingly.  So, to make datetime field independent of any specific timezone (more specifically - server timezone) , we needed a custom field that stores UTC (Universal Coordinated Time) , a globally unique datetime, as raw value and presents timezone specific date and time to the content authors.

Note: We can easily do conversion of UTC to any timezone and visa versa, with .NET globalization API


How this prototype works?

Here is how the new field looks to content authors.


This is what sitecore stores in backend.


You can notice the raw value is the UTC equivalent of the IST time content authors can see. And this is the secret of this field :)

You might be wondering from where the the timezone India Standard Time is coming??
 -  In our application, we have separated all settings from actual content items, and those settings are globally applicable to entire site (something like AppSettings in .net application)

-  So here the timezone IST is coming from site level settings, which looks like this



-  Based on above selection, only the display date and time will change, but the raw value will remain same.


Lets start creating custom date time field with timezone

Step 1: Add DatetimeZone field in core db

  • Switch to core db
  • Add item DateTimeZone at /sitecore/system/Field Types/Simple Types/
  • Add field values as shown below. (You are free to keep Assembly and Class names as per your requirements)





Step 2: Add Custom Logic for this field

For this field I have used the code of Date field of sitecore and added my custom logic. Use reflector to check the code of Sitecore.Shell.Applications.ContentEditor.Date

Here is my code,
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;
using System;
using System.Web.UI;
namespace MyCustomFields
{
    public class DateTimeZone : Input, IContentField
    {
        private DateTimePicker picker;
        public string ItemID
        {
            get
            {
                return base.GetViewStateString("ItemID");
            }
            set
            {
                Assert.ArgumentNotNull(value, "value");
                base.SetViewStateString("ItemID", value);
            }
        }
        public string RealValue
        {
            get
            {
                return base.GetViewStateString("RealValue");
            }
            set
            {
                Assert.ArgumentNotNull(value, "value");
                base.SetViewStateString("RealValue", value);
            }
        }
        public bool ShowTime
        {
            get
            {
                return base.GetViewStateBool("Showtime", false);
            }
            set
            {
                base.SetViewStateBool("Showtime", value);
            }
        }
        public bool IsModified
        {
            get
            {
                return System.Convert.ToBoolean(base.ServerProperties["IsModified"]);
            }
            protected set
            {
                base.ServerProperties["IsModified"] = value;
            }
        }
        public DateTimeZone()
        {
            this.Class = "scContentControl";
            base.Change = "#";
            base.Activation = true;
            this.ShowTime = true;
        }
        private string GetTimezoneAcronymOfSite()
        {
            return "IST";
        }
        private string GetTimeZoneofSite()
        {
            return "India Standard Time";
        }
        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            base.HandleMessage(message);
            if (!(message["id"] != this.ID))
            {
                string name;
                if ((name = message.Name) != null)
                {
                    if (name == "contentdate:today")
                    {
                        this.Today();
                    }
                    else if (name == "contentdate:clear")
                    {
                        this.ClearField();
                    }
                }
            }
        }
        public string GetValue()
        {
            return this.RealValue;
        }
        public void SetValue(string value)
        {
            Assert.ArgumentNotNull(value, "value");
            this.RealValue = value;
            if (this.picker != null)
            {
                this.picker.Value = (DateUtil.IsIsoDate(value) ? this.ConvertDateTimeFromUTC(value, this.GetTimeZoneofSite()) : value);
            }
        }
        protected override Item GetItem()
        {
            return Client.ContentDatabase.GetItem(this.ItemID);
        }
        protected override bool LoadPostData(string value)
        {
            bool result;
            if (base.LoadPostData(value))
            {
                this.picker.Value = (value ?? string.Empty);
                result = true;
            }
            else
            {
                result = false;
            }
            return result;
        }
        protected override void SetModified()
        {
            base.SetModified();
            this.IsModified = true;
            if (base.TrackModified)
            {
                Sitecore.Context.ClientPage.Modified = true;
            }
        }
        protected string GetCurrentDate()
        {
            string isoUtcNow = this.ConvertDateTimeToUTC(DateUtil.IsoNow, TimeZoneInfo.Local.Id);
            return this.ConvertDateTimeFromUTC(isoUtcNow, this.GetTimeZoneofSite());
        }
        private void ClearField()
        {
            this.SetRealValue(string.Empty);
        }
        protected void SetRealValue(string realvalue)
        {
            if (realvalue != this.RealValue)
            {
                this.SetModified();
            }
            this.RealValue = (DateUtil.IsIsoDate(realvalue) ? this.ConvertDateTimeToUTC(realvalue, this.GetTimeZoneofSite()) : realvalue);
            this.picker.Value = realvalue;
        }
        private void Today()
        {
            this.SetRealValue(this.GetCurrentDate());
        }
        protected override void OnInit(EventArgs e)
        {
            this.picker = new DateTimePicker();
            this.picker.ID = this.ID + "_picker";
            this.Controls.Add(this.picker);
            if (!string.IsNullOrEmpty(this.RealValue))
            {
                this.picker.Value = this.RealValue;
            }
            this.picker.Changed += delegate(object param0, EventArgs param1)
            {
                System.DateTime dtSelected = DateUtil.IsoDateToDateTime(this.picker.Value);
                if (this.IsInvalidTime(dtSelected, this.GetTimeZoneofSite()))
                {
                    SheerResponse.Alert("Selected time conflicts with Day Light saving start time. Please select different time.", new string[0]);
                    this.picker.Time = string.Empty;
                }
                this.SetRealValue(this.picker.Value);
            };
            this.picker.ShowTime = this.ShowTime;
            this.picker.Disabled = this.Disabled;
            base.OnInit(e);
        }
        protected override void OnLoad(EventArgs e)
        {
            LiteralControl ltrlTimeZone = new LiteralControl(string.Concat(new string[]
            {
                "<span style='font-weight:bold; padding-left:5px'>",
                this.GetTimezoneAcronymOfSite(),
                " (",
                this.GetTimeZoneofSite(),
                ")</span>"
            }));
            this.Controls.Add(ltrlTimeZone);
            base.OnLoad(e);
        }
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            base.ServerProperties["Value"] = base.ServerProperties["Value"];
            base.ServerProperties["RealValue"] = base.ServerProperties["RealValue"];
        }
        public string ConvertDateTimeFromUTC(string utcDate, string clientTimeZoneId)
        {
            string date = utcDate;
            if (utcDate != string.Empty)
            {
                System.DateTime dtUTCTime = DateUtil.IsoDateToDateTime(utcDate);
                System.DateTime dtDisplayDate = TimeZoneInfo.ConvertTimeFromUtc(dtUTCTime, TimeZoneInfo.FindSystemTimeZoneById(clientTimeZoneId));
                date = DateUtil.ToIsoDate(dtDisplayDate);
            }
            return date;
        }
        public bool IsInvalidTime(System.DateTime dtInput, string timeZoneId)
        {
            TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
            return timeZone == null || timeZone.IsInvalidTime(dtInput);
        }
        public string ConvertDateTimeToUTC(string date, string sourceTimeZoneId)
        {
            string utcDate = date;
            if (date != string.Empty)
            {
                System.DateTime dtSelectedDate = DateUtil.IsoDateToDateTime(date);
                System.DateTime dtUTCDate = TimeZoneInfo.ConvertTimeToUtc(dtSelectedDate, TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId));
                utcDate = DateUtil.ToIsoDate(dtUTCDate);
            }
            return utcDate;
        }
    }
}

 You can download the code file from here : DateTimeZone.cs

 Please do share your thoughts on this. Till then  Keep Sharing, Keep Learning :)

More References:

No comments:

Post a Comment