Infosys Microsoft Alliance and Solutions blog

« WPF - Text clips on some Vista machines | Main | If everything could be hosted! »

Silverlight - Preventing direct editing in DatePicker control

In my earlier blog I mentioned about the custom DatePicker control. Towards the end where I mentioned about making the textbox read only, I also mentioned about not being able to edit this template in Expression Blend 2.5 June CTP. On the forums, someone clarified that since this control doesn't resides in System.Windows.dll, its template cannot be edited in current version of Blend.

One can however include the template as mentioned in MSDN and include that in the project and modify suitably. When I included that in a test project, I ended up with System.Stack.OverflowException. That can however be fixed easily as I mentioned here.

Not only this, using this template, causes many other warnings/errors in VS and Expression also no longer shows the design time view of the control. Though, surprisingly, after fixing the stack overflow exception, I was able to run the code inspite of the many issues shown by VS.

Going back to my custom control, I first thought to add the template to the control and then make the textbox (which is actually DatePickerTextBox) as read only. However, I continued to face more issues in this.

Lutz Roeder's Reflector, as always, came to the rescue and I got an idea after looking at the implementation of OnApplyTemplate in the DatePicker control. The idea was to extract the DatePickerTextBox and then set it's property in code. To do this, I first added another property to my control (see the code so far in the previous blog)

        private bool _directEdit;

        public bool IsDirectEditingAllowed

        {

            get { return _directEdit; }

            set

            {

                _directEdit = value;

                if (_box != null)

                    _box.IsReadOnly = !value;

            }

        }

The _box variable is the DatePickerTextBox control that is set as part of the template and I obtain it as follows.

        public override void OnApplyTemplate()

        {

            base.OnApplyTemplate();

            _box = base.GetTemplateChild("TextBox") as DatePickerTextBox;

            if (_box != null)

                _box.IsReadOnly = !IsDirectEditingAllowed;

        }

I need to handle the setting of the IsReadOnly property on the DatePickerTextBox both when applying the template and when changing the IsDirectEditingAllowed property. With this, I can now set the property to false and thus make date selection possible only by using the calendar control. This addresses the issue of user making changes to the control directly by typing into it and thus impacting the values set using DisplayStartDate property.

For your reference, find the complete code for the custom DatePicker control below.

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;

using System.IO;

using System.Windows.Markup;

using System.Diagnostics;

using System.Globalization;

 

namespace Atul.Utilities

{

    public class StartDatePicker : DatePicker

    {

        DateTime _prevStartDate, _prevEndDate;

        DatePickerTextBox _box = null;

 

        public StartDatePicker()

        {

            MinimumDifferenceWithEndDate = 1; // default to 1 day

            IsInitializedToToday = true;

            RetainDifferenceOnDateChange = true;

            IsDirectEditingAllowed = true;

            this.Loaded += new RoutedEventHandler(StartDatePicker_Loaded);

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

        }

 

        /// <summary>

        /// Use this to get the DatePickerTextBox control and set its IsReadOny property. Needless to say

        /// that this will not work if the template is modified completely to not use DatePickerTextBox or

        /// it is used but with a different name

        /// </summary>

        public override void OnApplyTemplate()

        {

            base.OnApplyTemplate();

            _box = base.GetTemplateChild("TextBox") as DatePickerTextBox;

            if (_box != null)

                _box.IsReadOnly = !IsDirectEditingAllowed;

        }

 

        /// <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 bool _directEdit;

        /// <summary>

        /// Set this property appropriately to allow or not allow user to be able to directly type into the

        /// text box of DatePicker control

        /// </summary>

        public bool IsDirectEditingAllowed

        {

            get { return _directEdit; }

            set

            {

                _directEdit = value;

                if (_box != null)

                    _box.IsReadOnly = !value;

            }

        }

 

        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;

            }

        }

    }

}

Comments welcome !

Comments

I am not aware about the exact functionality you want to achieve, but to make the datapicker control readonly .. simply you do like this :
void DatePicker_KeyDown(object sender, KeyEventArgs e)
{
e.Handled = true;
}

So whatever the user input it will be shown.

Thanks for the suggestion Faiyaz. The only minor point will be that it still raises the event, while making the textbox part read only will prevent that. A bit more optimized, I guess !

I have a datepicker in struts2.0 with some default date.But when i reset the form then no date is displayed in the datepicker text box.Please help me in this regard.

One more workaround (continuing with Faiyaz's suggestion:

In order to make sure that user does not copy and paste or delete some data into/from the textbox of datepicker (after selecting a date), you can try this.

string date = "";

private void datePicker_SelectedDateChanged(object sender, SelectionChangedEventArgs e)
{
date = datePicker.SelectedDate.Value.ToShortDateString();
}

private void datePicker_KeyUp(object sender, KeyEventArgs e)
{
datePicker.Text = date;
}


So, as soon as user releases the key, the date selected will be back into the textbox.

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