Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A couple of tricks when using the standard WPF .NET 4.0 DatePicker control

0.00/5 (No votes)
9 Mar 2012 4  
Show hows to alter the DatePicker to use keyboard up/down keys for cleverer date selection.

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:

WPFDatePickerEx/Open.png

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
{
    /// <summary>
    /// Allows us to hook into when a new style is applied, so we can call ApplyTemplate()
    /// at the correct time to get the things we need out of the Template
    /// </summary>
    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); //unhook
    textBox.PreviewKeyDown += new KeyEventHandler(DatePickerTextBox_PreviewKeyDown); //hook
}

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: // dd/mm/yyyy
                delimeter = '/';
                break;
            case DatePickerFormat.Long:  // day month  year
                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: // Day
                result = result.AddDays(direction);
                break;
            case 1: // Month
                result = result.AddMonths(direction);
                break;
            case 2: // Year
                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));
                //this.Text =  result.ToShortDateString();
                break;
            case DatePickerFormat.Long:
                this.Text = string.Format(CultureInfo.CurrentCulture, 
                            result.ToString(dtfi.LongDatePattern, dtfi));
                //this.Text = result.ToLongDateString();
                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

    /// <summary>
    /// BlackOutDatesTextLookup : Stores dictionary to allow lookup of
    /// Calendar.BlackoutDates to reason for blackout dates string.
    /// </summary>
    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:

<!-- CalendarDayButton Style -->
<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>

                    <!-- No ToolTip highlighting if IValueConverter returned null -->
                    <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 Calendars 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

    /// <summary>
    /// Gets a tool tip for a date passed in. Could also return null
    /// </summary>
    /// The 'values' array parameter has the following elements:
    /// 
    /// - values[0] = Binding #1: The date to be looked up.
    /// This should be set up as a pathless binding; 
    ///   the Calendar control will provide the date.
    /// 
    /// - values[1] = Binding #2: A binding reference
    /// to the Calendar control that is invoking this converter.
    /// </remarks>
    public object Convert(object[] values, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        // Exit if values not set
        if ((values[0] == null) || (values[1] == null)) return null;

        // Get values passed in
        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;
    }

    /// <summary>
    /// Not used.
    /// </summary>
    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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here