Introduction
This is actually a pretty simple article, and to be honest, I ummed and ahhed about even making this an article at all. I thought it might make a 1/2 decent blog post, but in the end, I thought that it would benefit more people as a published article, so I decided to publish this dead simple article as an article and not a blog post; sorry for its shortness. I know it's not my normal style article, but hope that is OK; I just felt it was useful is all.
So what does the attached code actually do? Well, it is dead simple; it shows you how to modify the standard .NET 4.0 WPF DatePicker
control to allow the user to use the keyboard up/down keys to skip days/months/years when the mouse is in the correct portions of the DatePicker
text, which is something the standard control does not actually do. It also takes into account any black out dates that may currently be applied, and also shows you how to create a custom Calendar
style such that you can place a "Go To Today" button on it.
So that is what we are trying to do. As I say, dead simple, but surprisingly useful, and something that does not come out of the box, and some folk may not know how to do this, so I thought it was worth writing up.
What Does It Look Like
So what does it all look like then? Not surprising, not that interesting, it's a DatePicker
which also allows a "Go To Today" button on it. Here it is in all its glory. Freekin ace no (no you say, true code I say).
Here is the Calendar
when it is open:
Notice the "Go To Today" button. When the "Go To Today" button is clicked, it will unsurprisingly navigate the Calendar
and thus the DatePicker
to today's date, unless today's date is one of the black out dates, in which case, the "Go To Today" button will not be enabled.
And here is the Calendar
again, but this time, I am showing it with some added black out dates to the Calendar
, which you can see are marked as small x's in the image below.
The image shown below shows you something you will have to use a bit of imagination to figure out; basically, if your mouse is over the day part of the date and you press the Up key, the day part of the DatePicker
date will increase by one, unless it hits a black out date, in which case, it will advance to the next available non black-out date. The same logic is applied when the Down key is pressed. It works this way for the day/month/year parts.
How Does It All Work
So how does it work then? Well, it's pretty simple, and can be broken down into a few steps:
Step 1: Create a Specialized DatePicker
This step could not be easier; simply inherit from DatePicker
, as follows:
public class DatePickerEx : DatePicker
{
public static readonly DependencyProperty AlternativeCalendarStyleProperty =
DependencyProperty.Register("AlternativeCalendarStyle",
typeof(Style), typeof(DatePickerEx),
new FrameworkPropertyMetadata((Style)null,
new PropertyChangedCallback(OnAlternativeCalendarStyleChanged)));
public Style AlternativeCalendarStyle
{
get { return (Style)GetValue(AlternativeCalendarStyleProperty); }
set { SetValue(AlternativeCalendarStyleProperty, value); }
}
private static void OnAlternativeCalendarStyleChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
DatePickerEx target = (DatePickerEx)d;
target.ApplyTemplate();
}
}
We also want our specialized DatePicker
to be able to accept a new Style for the Calendar
, so that is why we include a new DependencyProperty
to allow for this, which in turn allows us to call ApplyTemplate()
when we want to. It is basically so we can control when to call ApplyTemplate()
, when we know all the required ControlTemplate
parts will be available.
Step 2: Create a Calendar Style to Replace the Default One
As we want a "Go To Today" button, we simply create a new Calendar
Style in the XAML to support this new button. Here is the full new Style for the Calendar
:
<Style x:Key="calendarWithGotToTodayStyle"
TargetType="{x:Type Calendar}">
<Setter Property="Foreground"
Value="#FF333333" />
<Setter Property="Background"
Value="White" />
<Setter Property="BorderBrush"
Value="DarkGray" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="CalendarDayButtonStyle"
Value="{StaticResource CalendarDayButtonStyleEx}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Calendar}">
<Border BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}">
<StackPanel Orientation="Vertical">
<StackPanel x:Name="PART_Root"
HorizontalAlignment="Center">
<CalendarItem x:Name="PART_CalendarItem"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Style="{TemplateBinding CalendarItemStyle}" />
<Button x:Name="PART_GoToTodayButton"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="10"
Content="Go To Today" />
</StackPanel>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Which we apply to our new DependencyProperty
, as follows:
<local:DatePickerEx
AlternativeCalendarStyle="{StaticResource calendarWithGotToTodayStyle}" />
Step 3: Write Some Code to Do the Work
All we have to do now is create some code to pluck out the relevant parts of the overall DatePicker
ControlTemplate
and also the new Style applied to the Calendar
which we just saw above.
How do we get what we want out of the DatePicker ControlTemplate
? Well, that is done as follows:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
textBox = this.GetTemplateChild("PART_TextBox") as DatePickerTextBox;
popup = this.GetTemplateChild("PART_Popup") as Popup;
if (AlternativeCalendarStyle != null)
{
System.Windows.Controls.Calendar calendar = popup.Child as System.Windows.Controls.Calendar;
calendar.Style = AlternativeCalendarStyle;
calendar.ApplyTemplate();
goToTodayButton = calendar.Template.FindName("PART_GoToTodayButton", calendar) as Button;
if (goToTodayButton != null)
{
gotoTodayCommand = new SimpleCommand(CanExecuteGoToTodayCommand, ExecuteGoToTodayCommand);
goToTodayButton.Command = gotoTodayCommand;
}
}
textBox.PreviewKeyDown -= new KeyEventHandler(DatePickerTextBox_PreviewKeyDown); textBox.PreviewKeyDown += new KeyEventHandler(DatePickerTextBox_PreviewKeyDown); }
And all that we now need to do is respond to the Up/Down keys and make sure we update the date taking into account any black out dates; this code is as follows:
private void DatePickerTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Up || e.Key == Key.Down)
{
int direction = e.Key == Key.Up ? 1 : -1;
string currentDateText = Text;
DateTime result;
if (!DateTime.TryParse(Text, out result))
return;
char delimeter = ' ';
switch (this.SelectedDateFormat)
{
case DatePickerFormat.Short: delimeter = '/';
break;
case DatePickerFormat.Long: delimeter = ' ';
break;
}
int index = 3;
bool foundIt = false;
for (int i = Text.Length - 1; i >= 0; i--)
{
if (Text[i] == delimeter)
{
--index;
if (textBox.CaretIndex > i)
{
foundIt = true;
break;
}
}
}
if (!foundIt)
index = 0;
switch (index)
{
case 0: result = result.AddDays(direction);
break;
case 1: result = result.AddMonths(direction);
break;
case 2: result = result.AddYears(direction);
break;
}
while (this.BlackoutDates.Contains(result))
{
result = result.AddDays(direction);
}
DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat();
switch (this.SelectedDateFormat)
{
case DatePickerFormat.Short:
this.Text = string.Format(CultureInfo.CurrentCulture,
result.ToString(dtfi.ShortDatePattern, dtfi));
break;
case DatePickerFormat.Long:
this.Text = string.Format(CultureInfo.CurrentCulture,
result.ToString(dtfi.LongDatePattern, dtfi));
break;
}
switch (index)
{
case 1:
textBox.CaretIndex = Text.IndexOf(delimeter) + 1;
break;
case 2:
textBox.CaretIndex = Text.LastIndexOf(delimeter) + 1;
break;
}
}
}
Extra Stuff: Showing Tooltips for Blackout Dates
I also decided to show you how to show ToolTips for your blackout dates. To do this, there are a couple of steps.
Step 1: Create an Attached DP Lookup
The first step is to create an attached DP that can be applied firstly to the DatePicker
, which will in turn pass the value of this attached DP on to the DatePicker
owned Calendar
. Here is the attached DP that I conjured up:
public static class CalendarProps
{
#region BlackOutDatesTextLookup
public static readonly DependencyProperty BlackOutDatesTextLookupProperty =
DependencyProperty.RegisterAttached("BlackOutDatesTextLookup",
typeof(Dictionary<CalendarDateRange, string>), typeof(CalendarProps),
new FrameworkPropertyMetadata(new Dictionary<CalendarDateRange, string>()));
public static Dictionary<CalendarDateRange,
string> GetBlackOutDatesTextLookup(DependencyObject d)
{
return (Dictionary<CalendarDateRange, string>)
d.GetValue(BlackOutDatesTextLookupProperty);
}
public static void SetBlackOutDatesTextLookup(DependencyObject d,
Dictionary<CalendarDateRange, string> value)
{
d.SetValue(BlackOutDatesTextLookupProperty, value);
}
#endregion
}
Step 2: Make Sure We Populate This Attached DP When We Add Blackout Dates
This is easily done. This is how it is done in the demo app:
public MainWindow()
{
InitializeComponent();
AddBlackOutDates(mdp, 2);
}
private void AddBlackOutDates(DatePicker dp, int offset)
{
Dictionary<CalendarDateRange, string> blackoutDatesTextLookup =
new Dictionary<CalendarDateRange, string>();
for (int i = 0; i < offset; i++)
{
CalendarDateRange range = new CalendarDateRange(DateTime.Now.AddDays(i));
dp.BlackoutDates.Add(range);
blackoutDatesTextLookup.Add(range,
string.Format("This is a simulated BlackOut date {0}",
range.Start.ToLongDateString()));
}
dp.SetValue(CalendarProps.BlackOutDatesTextLookupProperty, blackoutDatesTextLookup);
}
See how we apply this attached DP to our specialized DatePicker
. How is the Calendar
made aware of these? Well, we need to go back a step now and have another look at the ApplyTemplate()
method of our specialized DatePicker
. The part that ripples the attached DP values from the specialized DatePicker
to the Calendar
is shown below:
public override void OnApplyTemplate()
{
....
....
if (AlternativeCalendarStyle != null)
{
....
....
System.Windows.Controls.Calendar calendar =
popup.Child as System.Windows.Controls.Calendar;
calendar.SetValue(CalendarProps.BlackOutDatesTextLookupProperty,
this.GetValue(CalendarProps.BlackOutDatesTextLookupProperty));
....
....
}
....
....
}
Step 3: Make Sure the Calendar Has the Specialized CalendarDayButtonStyle
We now need to make sure that the specialized DatePicker
's embedded Calendar
has a special CalendarDayButtonStyle
Style. This is done as follows:
<Style x:Key="calendarWithGotToTodayStyle"
TargetType="{x:Type Calendar}">
....
....
<Setter Property="CalendarDayButtonStyle"
Value="{StaticResource CalendarDayButtonStyleEx}" />
....
....
</Style>
Where the most important parts of the CalendarDayButtonStyle
Style are as follows:
<!---->
<Style x:Key="CalendarDayButtonStyleEx"
TargetType="{x:Type CalendarDayButton}">
<Setter Property="MinWidth"
Value="5" />
<Setter Property="MinHeight"
Value="5" />
<Setter Property="FontSize"
Value="10" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CalendarDayButton}">
<Grid x:Name="CalendarDayButtonGrid">
<Grid.ToolTip>
<MultiBinding Converter="{local:HighlightDateConverter}">
<MultiBinding.Bindings>
<Binding />
<Binding RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type Calendar}}" />
</MultiBinding.Bindings>
</MultiBinding>
</Grid.ToolTip>
........
........
........
........
........
<Rectangle x:Name="TodayBackground"
Fill="#FFAAAAAA"
Opacity="0"
RadiusY="1"
RadiusX="1" />
<Rectangle x:Name="AccentBackground"
RadiusX="1"
RadiusY="1"
IsHitTestVisible="False"
Fill="{Binding RelativeSource=
{RelativeSource AncestorType=Calendar},
Path=DateHighlightBrush}" />
<Rectangle x:Name="SelectedBackground"
Fill="#FFBADDE9"
Opacity="0"
RadiusY="1"
RadiusX="1" />
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" />
<Rectangle x:Name="HighlightBackground"
Fill="#FFBADDE9"
Opacity="0"
RadiusY="1"
RadiusX="1" />
<ContentPresenter x:Name="NormalText"
TextElement.Foreground="#FF333333"
HorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
Margin="5,1,5,1"
VerticalAlignment=
"{TemplateBinding VerticalContentAlignment}" />
<Path x:Name="Blackout"
Data="M8.1772461,11.029181 L10.433105,
11.029181 L11.700684,12.801641
L12.973633,11.029181 L15.191895,
11.029181 L12.844727,13.999395
L15.21875,17.060919 L12.962891,
17.060919 L11.673828,15.256231
L10.352539,17.060919 L8.1396484,
17.060919 L10.519043,14.042364 z"
Fill="#FF000000"
HorizontalAlignment="Stretch"
Margin="3"
Opacity="0"
RenderTransformOrigin="0.5,0.5"
Stretch="Fill"
VerticalAlignment="Stretch" />
<Rectangle x:Name="DayButtonFocusVisual"
IsHitTestVisible="false"
RadiusY="1"
RadiusX="1"
Stroke="#FF45D6FA"
Visibility="Collapsed" />
</Grid>
<ControlTemplate.Triggers>
<!---->
<DataTrigger Value="{x:Null}">
<DataTrigger.Binding>
<MultiBinding Converter=
"{local:HighlightDateConverter}">
<MultiBinding.Bindings>
<Binding />
<Binding RelativeSource=
"{RelativeSource FindAncestor,
AncestorType={x:Type Calendar}}" />
</MultiBinding.Bindings>
</MultiBinding>
</DataTrigger.Binding>
<Setter TargetName="AccentBackground"
Property="Visibility"
Value="Hidden" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The main things to note here are that there is a ToolTip applied (near the top of the Style), which gets its value through a HighlightDateConverter IValueConverter
and that if the HighlightDateConverter IValueConverter
value is "{x:Null}" (see the Triggers section above), there is no ToolTip visible.
Step 4: Create the ToolTip
The last peice of the puzzle is to create the ToolTip. This is achived by passing the current date for the Day button and the whole Calendar
through a HighlightDateConverter IValueConverter
. Where the HighlightDateConverter IValueConverter
will see if the current date for the Day button is in Calendar.BlackOutDates
range. And if it is found, it will use the found range item to index into the Calendar
s BlackOutDatesTextLookup
attached DP which we setup earlier.
Here is the full listing for the HighlightDateConverter IValueConverter
:
public class HighlightDateConverter : MarkupExtension, IMultiValueConverter
{
#region MarkupExtension Overrides
private static HighlightDateConverter converter = null;
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (converter == null)
{
converter = new HighlightDateConverter();
}
return converter;
}
#endregion
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if ((values[0] == null) || (values[1] == null)) return null;
DateTime targetDate = (DateTime)values[0];
Calendar calendar = (Calendar)values[1];
var range = calendar.BlackoutDates.Where(x => x.Start.IsSameDateAs(targetDate));
if (range.Count() > 0)
{
Dictionary<CalendarDateRange, string> blackOutDatesTextLookup =
(Dictionary<CalendarDateRange, string>)
calendar.GetValue(CalendarProps.BlackOutDatesTextLookupProperty);
return blackOutDatesTextLookup[range.First()];
}
else
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
return new object[0];
}
#endregion
}
Special Thanks
For the tooltip stuff, I have based some of what I wrote on the most excellent article by David Veeneman, which is available at the following URL: ExtendingWPFCalendar.aspx.
That's All
If you found this useful, a vote/comment would be appreciated.