This is a continuation of my previous post, about WFFM and its quirks (or "bugs", or "features", depending on your point of view).

The Date Selector field

First of all, there are two fields for date picking: a "Date" field, that outputs three dropdowns and a "DatePicker" field that outputs a text field with a jQuery UI DatePicker attached to it. This post will be dealing with the former, though I can see that the latter might have its own issues as well.

Odd HTML output.

The html output has a rather odd structure, where there is 

  • a "span" (actually, an "asp:Label" without an associated control) that takes the content of the "title" field.
  • Three "label" tags, one for "year", one for "month" and one for "day" dropdowns.
  • Respectively, three dropdowns (year, month, day).

There is no additional markup for the labels and dropdowns, They are just output like that, one next to the other, without any grouping between them, they are all sibling HTML elements, with the three labels preceding the three dropdowns. No wrapping div for each label-dropdown pair. That's a lot of fun if you have even the slightest notion of styling the thing.

 

Undocumented and "Un-interfaced" properties

I mentioned three labels: "year", "month", "day". There is no way to set the values for these labels in a multilingual site, using the form designer. You have to decompile the dll, realize that there are three public properties named "YearLabel", MonthLabel"  and "DayLabel", and then go to Sitecore Content Editor, go to the "Date" field item in your form, and pull your XML-fu to write this snippet in the "Localized Parameters" for the field

<DayLabel>Day</DayLabel><MonthLabel>Month</MonthLabel><YearLabel>Year</YearLabel>

where you replace the node values with what you want written in the respective labels.

Ok, granted, that wasn't too difficult to find, but why hide the ability to edit the labels? I believe the Sitecore team simply forgot to add the required attributes to the properties ("VisualProperty", "DefaultValue", "Localizable"). However, it's really frustrating having to decompile the library just to find out how you're supposed to localize three labels, when  just a small code addition that already exists in other properties in the same field type would have made this a non-issue.

Date formats

There is a specific field in the form designer, called "Date Format". It's a dropdown, that shows a list of date display examples. The source for the droplist is at /sitecore/system/Modules/Web Forms for Marketers/Settings/Meta data/Date Formats. Usually, this is OK. However, for multilingual sites where one language prefers month before day (such as American English) whereas another one prefers day before month (such as pretty much the rest of the world), the fact that the specific parameter is not localized can (and does) pose a serious issue.As far as I know, there's no way around this without actually rolling out your own class, replacing the field type definition class in /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Simple Types/Date.

Multilingual month display

If you choose anything other than "MM" for month, you'll soon find out that the month dropdown always displays the months in English, no matter what language you are viewing the site in. This is due to the following code in the month dropdown population code (InitMonth metod):

		for (int i = 1; i <= 12; i++)
            {
                this.month.Items.Add(new ListItem(string.Format("{0:" + marker + "}", dateTime.AddMonths(i - 1)), i.ToString()));
            }

It should be changed to this:

            for (int i = 1; i <= 12; i++)
            {
                this.month.Items.Add(new ListItem(string.Format(Sitecore.Context.Language.CultureInfo, "{0:" + marker + "}", dateTime.AddMonths(i - 1)), i.ToString()));
            }

Unfortunately, the method is private, so to override it you need to roll out your own copy of the DateSelector class, and not just subclass the original.

 

Minimum/Maximum/Default date

there are three fields in the form designer to set dates:. Default, Minimum and Maximum. What these do is the following:

 

  • The Default date sets the date preselected when the page loads
  • The Minimum date sets a validation on the user selected date, as well as the minimum year in the relevant dropdown displayed in the form
  • The Maximum date sets a validation on the user selected date, as well as the maximum year in the relevant dropdown displayed in the form

Apart from an obscure "today" value to the default date, which I'm yet to find a way to pass using the form designer, there is absolutely no way to set these three dates in a rolling, dynamic fashion; you practically hard wire three fixed dates. However, most of the times I've had the pleasure (or torment, depending on your point of view) of creating a form with any kind of date selection, the limits and the default date were supposed to be fluid (e.g. from 2 years ago up until yesterday, with default value yesterday, or from the day after tomorrow until 3 months after today, with the default date being 3 days from now).

 

Unfortunately, the Sitecore UI for setting these fields (as well as the code supporting them) does not allow for such dynamic settings: the UI raises a calendar, while in the code, an ISO date string is expected. 

This is by far the worst of the quirks for the date selector field. This alone made me roll out my own class - effectively copying and then augmenting the existing class.

My custom class has the following new properties:

        [VisualCategory("Validation"), VisualProperty("Start Date expression:", 2001), DefaultValue("")]
        public string StartDateExp
        {
            get
            {
                return this.classAtributes["StartDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["StartDateExp"] = value;
            }
        }
        [VisualCategory("Validation"), VisualProperty("End Date expression:", 2001), DefaultValue("")]
        public string EndDateExp
        {
            get
            {
                return this.classAtributes["EndDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["EndDateExp"] = value;
            }
        }
        [VisualProperty("Selected Date expression:", 101), DefaultValue("")]
        public string SelectedDateExp
        {
            get
            {
                return this.classAtributes["SelectedDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["SelectedDateExp"] = value;
            }
        }

Replaced the InitYear method with the following:

        private void InitYear(string marker)
        {
            if (!string.IsNullOrEmpty(StartDateExp))
            {
                StartDate = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, StartDateExp));
            }
            if (!string.IsNullOrEmpty(EndDateExp))
            {
                EndDate = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, EndDateExp));
            }

            DateTime dateTime = new DateTime(this.StartDateTime.Year - 1, 1, 1);
            this.year.Items.Clear();
            for (int i = this.StartDateTime.Year; i <= this.EndDateTime.Year; i++)
            {
                dateTime = dateTime.AddYears(1);
                this.year.Items.Add(new ListItem(string.Format("{0:" + marker + "}", dateTime), i.ToString()));
            }
            this.year.SelectedValue = this.CurrentDate.Year.ToString();
        }

The fact that this particular method was marked as private in the original Date Selector class, made me copy the entire code from the decompiler and not just subclass the original.

I also replaced the OnInit with the following:

        protected override void OnInit(EventArgs e)
        {
            this.day.CssClass = "scfDateSelectorDay";
            this.month.CssClass = "scfDateSelectorMonth";
            this.year.CssClass = "scfDateSelectorYear";
            this.help.CssClass = "scfDateSelectorUsefulInfo";
            this.generalPanel.CssClass = "scfDateSelectorGeneralPanel";
            this.title.CssClass = "scfDateSelectorLabel";
            this.Controls.AddAt(0, this.generalPanel);
            this.Controls.AddAt(0, this.title);
            this.generalPanel.Controls.AddAt(0, this.help);
            List<string> list = new List<string>(this.DateFormat.Split(new char[]
            {
                '-'
            }));
            list.Reverse();
            if (!string.IsNullOrEmpty(this.SelectedDateExp))
            {
                SelectedDate =Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, SelectedDateExp));
            }
            list.ForEach(new Action<string>(this.InsertDateList));
            list.ForEach(new Action<string>(this.InsertLabelForDateList));
            this.RegisterCommonScript();
            this.month.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
            this.year.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
            this.day.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
        }

And added this:

        private DateTime ApplyTransform(DateTime d, string t)
        {
            var parts = t.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length>1)
            {
                foreach(var part in parts)
                {
                    d = ApplyTransform(d, part);
                }
                return d;
            }
            if (t=="today")
            {
                return DateTime.Now;
            }
            var num = int.Parse(t.Substring(1, t.Length - 2));
            if (t[0] == '-')
            {
                num = -num;
            }
            switch (t[t.Length-1])
            {
                case 'd':
                    return d.AddDays(num);
                case 'm':
                    return d.AddMonths(num);
                case 'y':
                    return d.AddYears(num);
                default:
                    return d;
            }
        }

This is the method that actually does things. First It reads the input from the "Start/End/Default date Expression", which is a string literal of space separated terms of the form (+/-)(number)(marker) e.g.

+2d -3m +1y

The above means "9 months and 2 days from today". The notation is taken from the similar properties of the jQuery UI DatePicker widget.

Then, it splits the input into terms, and applies the terms sequentially on today's date. Full code for the class at the end of this post.

But wait, that's not all! We've dealt with the display, but not the validation of the user input. you see, the dates the user can select using the dropdowns span from January 1st of the minimum year to December 31st of the maximum year. The minimum and maximum dates are more often than not different. There is a validator that handles this, however it also needs to be aware of the newly added "Expression" properties.

I followed the same logic for the validator, simply copied the code from the decompiler into a new class and added the following:

        public string EndDateExp
        {
            get
            {
                return this.classAttributes["enddateexp"];
            }
            set
            {
                this.classAttributes["enddateexp"] = value;
            }
        }

        public string StartDateExp
        {
            get
            {
                return this.classAttributes["startdateexp"];
            }
            set
            {
                this.classAttributes["startdateexp"] = value;
            }
        }
        private string ApplyTransform(string Modifier)
        {
            return Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, Modifier));
        }

        private DateTime ApplyTransform(DateTime date, string Modifier)
        {
            var parts = Modifier.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length > 1)
            {
                foreach (var part in parts)
                {
                    date = ApplyTransform(date, part);
                }
                return date;
            }
            if (Modifier == "today")
            {
                return DateTime.Today;
            }
            var num = int.Parse(Modifier.Substring(1, Modifier.Length - 2));
            if (Modifier[0] == '-')
            {
                num = -num;
            }
            switch (Modifier[Modifier.Length - 1])
            {
                case 'd':
                    return date.AddDays(num);
                case 'm':
                    return date.AddMonths(num);
                case 'y':
                    return date.AddYears(num);
                default:
                    return date;
            }
        }

And changed the OnLoad to the following:

        protected override void OnLoad(EventArgs e)
        {
            if (!string.IsNullOrEmpty(StartDateExp))
            {
                StartDate = ApplyTransform(StartDateExp);
            }
            if (!string.IsNullOrEmpty(EndDateExp))
            {
                EndDate = ApplyTransform(EndDateExp);
            }
            base.ErrorMessage = string.Format(base.ErrorMessage, "{0}", this.StartDateTime.ToString(Constants.LongDateFormat), this.EndDateTime.ToString(Constants.LongDateFormat));
            this.Text = string.Format(this.Text, "{0}", this.StartDateTime.ToString(Constants.LongDateFormat), this.EndDateTime.ToString(Constants.LongDateFormat));
            base.OnLoad(e);
        }

Full code for the validator class at the end of the post.

Sitecore-wise, I opted to create a new field and a new validator, rather than overwrite the existing.

That's it! And it works like a charm!

As promised, code for the DateSelector field:

    [Adapter(typeof(DateAdapter)), ValidationProperty("Value")]
    public class DateSelector : ValidateControl, IHasTitle
    {
        private static readonly string baseCssClassName = "scfDateSelectorBorder";

        protected DropDownList day = new DropDownList();

        protected Panel generalPanel = new Panel();

        protected DropDownList month = new DropDownList();

        protected System.Web.UI.WebControls.Label title = new System.Web.UI.WebControls.Label();

        protected DropDownList year = new DropDownList();

        private DateTime CurrentDate
        {
            get
            {
                DateTime result;
                if (string.IsNullOrEmpty(this.SelectedDate))
                {
                    result = Sitecore.DateUtil.ToServerTime(DateTime.UtcNow);
                }
                else
                {
                    result = Sitecore.DateUtil.IsoDateToDateTime(Sitecore.DateUtil.IsoDateToServerTimeIsoDate(this.SelectedDate));
                }
                return result;
            }
        }

        private DateTime StartDateTime
        {
            get
            {
                return Sitecore.DateUtil.IsoDateToDateTime(this.StartDate);
            }
        }

        private DateTime EndDateTime
        {
            get
            {
                return Sitecore.DateUtil.IsoDateToDateTime(this.EndDate);
            }
        }

        public string Value
        {
            get
            {
                DateTime dateTime = new DateTime(this.StartDateTime.Year + ((this.year.SelectedIndex > -1) ? this.year.SelectedIndex : 0), ((this.month.SelectedIndex > -1) ? this.month.SelectedIndex : 0) + 1, ((this.day.SelectedIndex > -1) ? this.day.SelectedIndex : 0) + 1);
                return Sitecore.DateUtil.ToIsoDate(dateTime.Date);
            }
        }

        [VisualFieldType(typeof(SelectDateField)), VisualProperty("Selected Date:", 100), DefaultValue("today")]
        public string SelectedDate
        {
            get;
            set;
        }
        [VisualProperty("Selected Date expression:", 101), DefaultValue("")]
        public string SelectedDateExp
        {
            get
            {
                return this.classAtributes["SelectedDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["SelectedDateExp"] = value;
            }
        }

        public override string DefaultValue
        {
            set
            {
                this.SelectedDate = value;
            }
        }

        [VisualFieldType(typeof(DateFormatField)), VisualProperty("Display Format:", 100), DefaultValue("yyyy-MMMM-dd")]
        public string DateFormat
        {
            get
            {
                return this.classAtributes["DateFormat"];
            }
            set
            {
                this.classAtributes["DateFormat"] = value;
            }
        }

        [VisualCategory("Validation"), VisualFieldType(typeof(SelectDateField)), VisualProperty("Start Date:", 2000), DefaultValue("20000101T000000")]
        public string StartDate
        {
            get
            {
                return this.classAtributes["startDate"];
            }
            set
            {
                if (!Sitecore.DateUtil.IsIsoDate(value))
                {
                    throw new ArgumentException("The value is not an iso date.", "value");
                }
                if (!string.IsNullOrEmpty(StartDateExp))
                {
                    this.classAtributes["startDate"] = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, StartDateExp));
                }
                else
                {
                    this.classAtributes["startDate"] = value;
                }
            }
        }

        [VisualCategory("Validation"), VisualProperty("Start Date expression:", 2001), DefaultValue("")]
        public string StartDateExp
        {
            get
            {
                return this.classAtributes["StartDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["StartDateExp"] = value;
            }
        }

        [VisualCategory("Validation"), VisualFieldType(typeof(SelectDateField)), VisualProperty("End Date:", 2000)]
        public string EndDate
        {
            get
            {
                return this.classAtributes["endDate"];
            }
            set
            {
                if (!Sitecore.DateUtil.IsIsoDate(value))
                {
                    throw new ArgumentException("The value is not an iso date.", "value");
                }
                if (!string.IsNullOrEmpty(EndDateExp))
                {
                    this.classAtributes["endDate"] = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, EndDateExp));
                }
                else
                {
                    this.classAtributes["endDate"] = value;
                }
            }
        }
        [VisualCategory("Validation"), VisualProperty("End Date expression:", 2001), DefaultValue("")]
        public string EndDateExp
        {
            get
            {
                return this.classAtributes["EndDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["EndDateExp"] = value;
            }
        }

        public override string ID
        {
            get
            {
                return base.ID;
            }
            set
            {
                this.title.ID = value + "_text";
                this.day.ID = value + "_day";
                this.month.ID = value + "_month";
                this.year.ID = value + "_year";
                base.ID = value;
            }
        }

        public override ControlResult Result
        {
            get
            {
                return new ControlResult(this.ControlName, this.Value, null);
            }
            set
            {
            }
        }

        [VisualFieldType(typeof(CssClassField)), VisualProperty("CSS Class:", 600), DefaultValue("scfDateSelectorBorder")]
        public new string CssClass
        {
            get
            {
                return base.CssClass;
            }
            set
            {
                base.CssClass = value;
            }
        }

        protected override Control InnerValidatorContainer
        {
            get
            {
                return this.generalPanel;
            }
        }

        protected override Control ValidatorContainer
        {
            get
            {
                return this;
            }
        }

        [Bindable(true), Description("Title")]
        public string Title
        {
            get
            {
                return this.title.Text;
            }
            set
            {
                this.title.Text = value;
            }
        }

        public string DayLabel
        {
            get;
            set;
        }

        public string MonthLabel
        {
            get;
            set;
        }

        public string YearLabel
        {
            get;
            set;
        }

        public DateSelector(HtmlTextWriterTag tag) : base(tag)
        {
            this.EnableViewState = false;
            this.CssClass = DateSelector.baseCssClassName;
            this.classAtributes["DateFormat"] = "yyyy-MMMM-dd";
            this.classAtributes["startDate"] = Sitecore.DateUtil.IsoDateToServerTimeIsoDate("20000101T120000");
            this.classAtributes["endDate"] = Sitecore.DateUtil.ToIsoDate(Sitecore.DateUtil.ToServerTime(DateTime.UtcNow.AddYears(1)));
            this.SelectedDate = Sitecore.DateUtil.ToIsoDate(DateTime.UtcNow);
            this.DayLabel = "Day";
            this.MonthLabel = "Month";
            this.YearLabel = "Year";
            Utils.SetUserCulture();
        }

        public DateSelector() : this(HtmlTextWriterTag.Div)
        {
        }

        public override void RenderControl(HtmlTextWriter writer)
        {
            this.DoRender(writer);
        }

        protected virtual void DoRender(HtmlTextWriter writer)
        {
            base.RenderControl(writer);
        }

        protected override void OnInit(EventArgs e)
        {
            this.day.CssClass = "scfDateSelectorDay";
            this.month.CssClass = "scfDateSelectorMonth";
            this.year.CssClass = "scfDateSelectorYear";
            this.help.CssClass = "scfDateSelectorUsefulInfo";
            this.generalPanel.CssClass = "scfDateSelectorGeneralPanel";
            this.title.CssClass = "scfDateSelectorLabel";
            this.Controls.AddAt(0, this.generalPanel);
            this.Controls.AddAt(0, this.title);
            this.generalPanel.Controls.AddAt(0, this.help);
            List<string> list = new List<string>(this.DateFormat.Split(new char[]
            {
                '-'
            }));
            list.Reverse();
            if (!string.IsNullOrEmpty(this.SelectedDateExp))
            {
                SelectedDate =Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, SelectedDateExp));
            }
            list.ForEach(new Action<string>(this.InsertDateList));
            list.ForEach(new Action<string>(this.InsertLabelForDateList));
            this.RegisterCommonScript();
            this.month.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
            this.year.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
            this.day.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
        }

        protected override void Render(HtmlTextWriter writer)
        {
            int i = this.day.Items.Count;
            while (i <= 31)
            {
                i++;
                this.Page.ClientScript.RegisterForEventValidation(this.day.UniqueID, i.ToString());
            }
            base.Render(writer);
        }

        protected override void OnPreRender(EventArgs e)
        {
            int num = this.CurrentDate.Year;
            if (string.IsNullOrEmpty(this.year.SelectedValue) || !int.TryParse(this.year.SelectedValue, out num))
            {
                num = this.CurrentDate.Year;
            }
            while (this.day.Items.Count > CultureInfo.InvariantCulture.Calendar.GetDaysInMonth(num, this.month.SelectedIndex + 1))
            {
                this.day.Items.RemoveAt(this.day.Items.Count - 1);
            }
            HiddenField hiddenField = new HiddenField
            {
                ID = (this.ID ?? string.Empty) + "_complexvalue",
                Value = this.Value
            };
            if (this.FindControl(hiddenField.ID) == null)
            {
                this.generalPanel.Controls.AddAt(0, hiddenField);
            }
            base.OnPreRender(e);
            this.title.AssociatedControlID = null;
        }

        private void InsertLabelForDateList(string marker)
        {
            char c = marker.ToLower()[0];
            if (c == 'd')
            {
                this.generalPanel.Controls.AddAt(0, new System.Web.UI.WebControls.Label
                {
                    AssociatedControlID = this.day.ID,
                    Text = Translate.Text(this.DayLabel ?? string.Empty),
                    CssClass = "scfDateSelectorShortLabelDay"
                });
                return;
            }
            if (c == 'm')
            {
                this.generalPanel.Controls.AddAt(0, new System.Web.UI.WebControls.Label
                {
                    AssociatedControlID = this.month.ID,
                    Text = Translate.Text(this.MonthLabel ?? string.Empty),
                    CssClass = "scfDateSelectorShortLabelMonth"
                });
                return;
            }
            if (c != 'y')
            {
                return;
            }
            this.generalPanel.Controls.AddAt(0, new System.Web.UI.WebControls.Label
            {
                AssociatedControlID = this.year.ID,
                Text = Translate.Text(this.YearLabel ?? string.Empty),
                CssClass = "scfDateSelectorShortLabelYear"
            });
        }

        private void InsertDateList(string marker)
        {
            char c = marker.ToLower()[0];
            if (c == 'd')
            {
                this.InitDay(marker);
                this.generalPanel.Controls.AddAt(0, this.day);
                return;
            }
            if (c == 'm')
            {
                this.InitMonth(marker);
                this.generalPanel.Controls.AddAt(0, this.month);
                return;
            }
            if (c != 'y')
            {
                return;
            }
            this.InitYear(marker);
            this.generalPanel.Controls.AddAt(0, this.year);
        }

        private void InitDay(string marker)
        {
            this.day.Items.Clear();
            for (int i = 1; i <= 31; i++)
            {
                this.day.Items.Add(new ListItem(i.ToString(), i.ToString()));
            }
            this.day.SelectedIndex = this.CurrentDate.Day - 1;
        }

        private void InitMonth(string marker)
        {
            DateTime dateTime = default(DateTime);
            this.month.Items.Clear();
            for (int i = 1; i <= 12; i++)
            {
                this.month.Items.Add(new ListItem(string.Format(Sitecore.Context.Language.CultureInfo, "{0:" + marker + "}", dateTime.AddMonths(i - 1)), i.ToString()));
            }
            this.month.SelectedIndex = this.CurrentDate.Month - 1;
        }

        private void InitYear(string marker)
        {
            if (!string.IsNullOrEmpty(StartDateExp))
            {
                StartDate = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, StartDateExp));
            }
            if (!string.IsNullOrEmpty(EndDateExp))
            {
                EndDate = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, EndDateExp));
            }

            DateTime dateTime = new DateTime(this.StartDateTime.Year - 1, 1, 1);
            this.year.Items.Clear();
            for (int i = this.StartDateTime.Year; i <= this.EndDateTime.Year; i++)
            {
                dateTime = dateTime.AddYears(1);
                this.year.Items.Add(new ListItem(string.Format("{0:" + marker + "}", dateTime), i.ToString()));
            }
            this.year.SelectedValue = this.CurrentDate.Year.ToString();
        }

        protected void RegisterCommonScript()
        {
        }

        private DateTime ApplyTransform(DateTime d, string t)
        {
            var parts = t.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length>1)
            {
                foreach(var part in parts)
                {
                    d = ApplyTransform(d, part);
                }
                return d;
            }
            if (t=="today")
            {
                return DateTime.Now;
            }
            var num = int.Parse(t.Substring(1, t.Length - 2));
            if (t[0] == '-')
            {
                num = -num;
            }
            switch (t[t.Length-1])
            {
                case 'd':
                    return d.AddDays(num);
                case 'm':
                    return d.AddMonths(num);
                case 'y':
                    return d.AddYears(num);
                default:
                    return d;
            }
        }
    }

And also for the validator

    public class DateValidator : FormCustomValidator
    {
        public string EndDate
        {
            get
            {
                return this.classAttributes["enddate"];
            }
            set
            {
                this.classAttributes["enddate"] = value;
            }
        }

        public string StartDate
        {
            get
            {
                return this.classAttributes["startdate"];
            }
            set
            {
                this.classAttributes["startdate"] = value;
            }
        }

        public string EndDateExp
        {
            get
            {
                return this.classAttributes["enddateexp"];
            }
            set
            {
                this.classAttributes["enddateexp"] = value;
            }
        }

        public string StartDateExp
        {
            get
            {
                return this.classAttributes["startdateexp"];
            }
            set
            {
                this.classAttributes["startdateexp"] = value;
            }
        }

        protected DateTime EndDateTime
        {
            get
            {
                return Sitecore.DateUtil.IsoDateToDateTime(this.EndDate).Date;
            }
        }

        protected DateTime StartDateTime
        {
            get
            {
                return Sitecore.DateUtil.IsoDateToDateTime(this.StartDate).Date;
            }
        }

        public DateValidator()
        {
            base.ServerValidate += new System.Web.UI.WebControls.ServerValidateEventHandler(this.OnDateValidate);
            this.StartDate = "20000101T000000";
            this.EndDate = Sitecore.DateUtil.ToIsoDate(DateTime.UtcNow.AddYears(1));
            Utils.SetUserCulture();
        }

        protected override bool EvaluateIsValid()
        {
            bool result;
            try
            {
                if (!string.IsNullOrEmpty(base.ControlToValidate))
                {
                    result = base.EvaluateIsValid();
                }
                else
                {
                    result = true;
                }
            }
            catch (ArgumentOutOfRangeException)
            {
                result = false;
            }
            return result;
        }

        protected override void OnLoad(EventArgs e)
        {
            if (!string.IsNullOrEmpty(StartDateExp))
            {
                StartDate = ApplyTransform(StartDateExp);
            }
            if (!string.IsNullOrEmpty(EndDateExp))
            {
                EndDate = ApplyTransform(EndDateExp);
            }
            base.ErrorMessage = string.Format(base.ErrorMessage, "{0}", this.StartDateTime.ToString(Constants.LongDateFormat), this.EndDateTime.ToString(Constants.LongDateFormat));
            this.Text = string.Format(this.Text, "{0}", this.StartDateTime.ToString(Constants.LongDateFormat), this.EndDateTime.ToString(Constants.LongDateFormat));
            base.OnLoad(e);
        }

        private void OnDateValidate(object source, System.Web.UI.WebControls.ServerValidateEventArgs args)
        {
            DateTime date = Sitecore.DateUtil.IsoDateToDateTime(args.Value).Date;
            if (this.StartDateTime <= date && date <= this.EndDateTime)
            {
                args.IsValid = true;
                return;
            }
            args.IsValid = false;
        }

        private string ApplyTransform(string Modifier)
        {
            return Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, Modifier));
        }

        private DateTime ApplyTransform(DateTime date, string Modifier)
        {
            var parts = Modifier.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length > 1)
            {
                foreach (var part in parts)
                {
                    date = ApplyTransform(date, part);
                }
                return date;
            }
            if (Modifier == "today")
            {
                return DateTime.Today;
            }
            var num = int.Parse(Modifier.Substring(1, Modifier.Length - 2));
            if (Modifier[0] == '-')
            {
                num = -num;
            }
            switch (Modifier[Modifier.Length - 1])
            {
                case 'd':
                    return date.AddDays(num);
                case 'm':
                    return date.AddMonths(num);
                case 'y':
                    return date.AddYears(num);
                default:
                    return date;
            }
        }
    }

Happy Coding!