Infosys Microsoft Alliance and Solutions blog

« ASP.NET, ASP.NET AJAX and Silverlight - Which way would you go? | Main | WPF - Text clips on some Vista machines »

Silverlight - Handling Start and End dates with Custom DatePicker Control

When working with DatePicker controls in application, very typically they get used to manage start and end dates. Like recently I was writing a tracking application in Silverlight and had the need to manage allocation and deallocation and hence start and end dates. There are typical requirements also associated with such implementations like start date cannot be in past, end date cannot be before start date.

Implementing this with Silverlight DatePicker controls was fairly trivial. If you haven't used these controls, you can find an introduction to them here. These controls also offer other interesting properties that I played with namely DisplayStartDate. I then thought that some of things I did in the logic, would be useful in multiple scenarios, so why not create a new custom DatePicker control that will have these functionalities in-built.

Creating custom controls is again pretty straightforward. Start by inheriting from the control you want to extend and then implement the necessary set of properties and methods required. Note that this is primarily the logic extension. For extending the UI, you will have to work with the templating features and Expression Blend will be a good tool for that. In my case, I am more interesting in the logic of the control and let the UI remain the same.

Some of the properties that I added to the control are as below. I added all these as regular CLR properties. If you need to support binding, these will have to be added as dependency properties.

IsInitializedToToday: The DatePicker does default's to today's date, but that is only set in the Calendar drop down. The textbox part of the control shows only the watermark (the date format) and the actual date is not displayed. To me this looks like a bug. Anyway, so I have added this property, which if set, will also show the current date in the textbox.

EndDateControl: This property points to the DatePicker control on the parent window that will be used to set the end date. This custom control itself will be acting as StartDate. Note that this property cannot be set in XAML and has to be set in code behind.

MinimumDifferenceWithEndDate: This property specifies what should be the minimum difference between start and end date. If this is say set to 5, and the start date is set to 10th, it will cause the End date to be set to dates beyond 15th. It appropriately changes the DisplayStartDate for the end date control (specified using EndDateControl property).

RetainDifferenceOnDateChange: This is a bit tricky to explain :-). As the start date is changed, the difference between it and end date changes. Now this should always be at a minimum of the value set using MinimumDifferenceWithEndDate. However take a case that the user has set the start date as today and explicitly end date as 20 days from today (and minimum difference is 1). Now the user comes back and changes the start date to today + 30 days. This causes the end date to be invalid. So if this property is set, end date will be reset to today + 30 + 20, thus retaining the earlier difference set by the user. An interesting point here is that this reset will trigger only if the difference is lower than mini days set using the MinimumDifferenceWithEndDate property since otherwise the end date isn't invalid. So If the user had reset to today + 15, the end date would have remained as is.

Below is the custom DatePicker control code. Feel free to use this as you see fit in your work.

using System;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Ink;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

 

namespace Atul.Utilities

{

    public class StartDatePicker : DatePicker

    {

        DateTime _prevStartDate, _prevEndDate;

 

        public StartDatePicker()

        {

            MinimumDifferenceWithEndDate = 1; // default to 1 day

            IsInitializedToToday = true;

            RetainDifferenceOnDateChange = true;

            this.Loaded += new RoutedEventHandler(StartDatePicker_Loaded);

            this.SelectedDateChanged += new EventHandler<SelectionChangedEventArgs>(StartDatePicker_SelectedDateChanged);

        }

 

        /// <summary>

        /// If set to True, control will default to today's date. If set to False, it will remain blank and use the

        /// DatePicker's default value The default value for this control is True

        /// </summary>

        public bool IsInitializedToToday { get; set; }

 

        /// <summary>

        /// This is a bit tricky to explain :-). As the start date is changed, the difference between it and end

        /// date changes. Now this should always be at a minimum of the value set using MinimumDifferenceWithEndDate.

        /// However take a case that the user has set the start date as today and explicitly end date as 20

        /// days from today (and minimum is 1). Now the user comes back and changes the start date to

        /// today + 30 days. This causes the end date to be invalid. So if this property is set, we will automatically

        /// reset the end date to today + 30 + 20, thus retaining the earlier difference set by the user. An

        /// interesting point here is that this reset will trigger only if the difference is lower than mini days

        /// set using the MinimumDifferenceWithEndDate property since otherwise the end date isn't invalid.

        /// So If the user had reset to today + 15, the end date would have remained as is

        /// </summary>

        public bool RetainDifferenceOnDateChange { get; set; }

 

        private int _minDiff;

        /// <summary>

        /// Get or Set # days difference that should be maintained between Start and End dates

        /// </summary>

        public int MinimumDifferenceWithEndDate

        {

            get { return _minDiff; }

            set

            {

                if (value < 0)

                    return; //should possibly throw an exception like out of range here

 

                _minDiff = value;

            }

        }

 

        private DatePicker _endDtControl = null;

        public DatePicker EndDatePicker

        {

            get { return _endDtControl; }

            set

            {

                if (value != null)

                {

                    _endDtControl = value;

                    _endDtControl.Loaded += new RoutedEventHandler(_endDtControl_Loaded);

                    _endDtControl.SelectedDateChanged += new EventHandler<SelectionChangedEventArgs>(_endDtControl_SelectedDateChanged);

                }

            }

        }

 

        private void StartDatePicker_Loaded(object sender, RoutedEventArgs e)

        {

            if (IsInitializedToToday)

            {

                DateTime today = DateTime.Today;

                _prevStartDate = today;

                this.DisplayDateStart = this.SelectedDate = today;

            }

        }

 

        void StartDatePicker_SelectedDateChanged(object sender, SelectionChangedEventArgs e)

        {

            if (_endDtControl != null && (_endDtControl.SelectedDate.HasValue))

            {

                DateTime dt = this.SelectedDate.Value;

                if (_endDtControl.SelectedDate.Value.Subtract(dt).Days < MinimumDifferenceWithEndDate)

                {

                    if (RetainDifferenceOnDateChange)

                    {

                        //reset the End date based on earlier date difference

                        TimeSpan diff = _prevEndDate.Subtract(_prevStartDate);

                        _endDtControl.SelectedDate = dt.AddDays(diff.Days);

                    }

                    else

                    {

                        _endDtControl.SelectedDate = dt.AddDays(MinimumDifferenceWithEndDate);

                    }

                }

                //the display start date for the end date control should be altered based on current selected start date

                //and should always be more than it by the factor of MinimumDifferenceWithEndDate

                _endDtControl.DisplayDateStart = dt.AddDays(MinimumDifferenceWithEndDate);

            }

            _prevStartDate = this.SelectedDate.Value;

        }

 

        void _endDtControl_Loaded(object sender, RoutedEventArgs e)

        {

            //initialize various values, based on today's date and mini difference

            _endDtControl.DisplayDateStart = _endDtControl.SelectedDate = _prevEndDate = DateTime.Today.AddDays(MinimumDifferenceWithEndDate);

        }

 

        /// <summary>

        /// Preview Selection changed would have been better, but since it isn't supported, we will work with

        /// Selected Date Changed event itself. The event fires twice for some reason i can't currently trace !

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        void _endDtControl_SelectedDateChanged(object sender, SelectionChangedEventArgs e)

        {

            //selection has changed, update the stored date. however ensure that is respects

            //MinimumDifferenceWithEndDate value

            TimeSpan diff = _endDtControl.SelectedDate.Value.Subtract(this.SelectedDate.Value);

            if (diff.Days < MinimumDifferenceWithEndDate)

            {

                //reject change and reset to previous date. We have previous date stored in _prevEndDate, but it

                //is also available as part of the event argument

                _endDtControl.SelectedDate = (DateTime)e.RemovedItems[0]; // this will cause this event to fire again !

            }

            else

            {

                _prevEndDate = _endDtControl.SelectedDate.Value;

            }

        }

    }

}

A few caveats
1. When DisplayStartDate is set, I would ideally expect to not be able to set dates before that date. This works fine if I am setting dates via the Calendar drop down. However, I can still go ahead and type any earlier date directly in the edit box. Interesting, this also causes the Calendar to now display dates all the way to this date. To me this looks like a bug in DatePicker control.

2. DatePicker doesn't display's today's date by default in the edit box, though the Calendar does this (already mentioned above). You need to do something like control.SelectedDate = DateTime.Today in code (constructor or loaded event handler) to get this to work correctly.

3. Based on point 1 above, let's say the user has set MinimumDifferenceWithEndDate as 5. When working with Calendar drop downs, this will be enforced, but user can directly type in a new date and this can break this setting. I did try to handle this by modifying the SelectedDateChanged handler as below, but for some reason the event seems to fire 4 times (I can understand twice, one due to direct edit and one due to my invoking SelectedDate). The end result is still what I want, but the event firing so many times obviously isn't right.

        void _endDtControl_SelectedDateChanged(object sender, SelectionChangedEventArgs e)

        {

            //selection has changed, update the stored date. however ensure that is respects

            //MinimumDifferenceWithEndDate value

            TimeSpan diff = _endDtControl.SelectedDate.Value.Subtract(this.SelectedDate.Value);

            if (diff.Days < MinimumDifferenceWithEndDate)

            {

                //reject change and reset to previous date. We have previous date stored in _prevEndDate, but it

                //is also available as part of the event argument

                _endDtControl.SelectedDate = (DateTime)e.RemovedItems[0]; // this will cause this event to fire again !

            }

            else

            {

                _prevEndDate = _endDtControl.SelectedDate.Value;

            }

        }

4. To address the direct editing issue, one option would be to edit the control template and make the edit box read only. That way the only way to enter date will be via the calendar drop down. To my surprise, when I tried to do this, I found the control template for DatePicker control to be empty. The template seems to be all built in code with no XAML based template being used. So right now, I don't have a solution of how to address this issue. If you know, please write back.

Finally to wrap up, I haven't done detailed testing on this and also there could be some more useful properties added to make this more functional. Do revert back with comments and I will be happy to make changes.

Comments

Hi ,
I want to create a custom time picker in silverlight 3.
Is there any example of this..

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)

Please key in the two words you see in the box to validate your identity as an authentic user and reduce spam.

Subscribe to this blog's feed

Follow us on

Blogger Profiles

Infosys on Twitter