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.
The _box variable is the DatePickerTextBox control that is set as part of the template and I obtain it as follows.
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;
}
}
}
}