Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

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

Rate me:
Please Sign up or sign in to vote.
4.88/5 (48 votes)
9 Mar 2012CPOL6 min read 209.7K   3K   62   45
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.

Image 2

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.

Image 3

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:

C#
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:

XML
<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:

XML
<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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

XML
<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:

XML
<!-- 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:

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionIt's so complex... Pin
Thornik26-May-17 9:33
Thornik26-May-17 9:33 
QuestionVisual Ancestor Bug? Pin
Jacquers23-Mar-15 4:10
Jacquers23-Mar-15 4:10 
AnswerRe: Visual Ancestor Bug? Pin
Sacha Barber23-Mar-15 4:33
Sacha Barber23-Mar-15 4:33 
GeneralRe: Visual Ancestor Bug? Pin
Jacquers23-Mar-15 4:36
Jacquers23-Mar-15 4:36 
QuestionAccessing Header button of date Picker Pin
Member 930805329-Oct-13 3:06
Member 930805329-Oct-13 3:06 
QuestionClosing the calendar when selecting 'Today' Pin
Jacquers1-Nov-12 3:16
Jacquers1-Nov-12 3:16 
QuestionPicture links broken? Pin
Slacker0079-Mar-12 7:24
professionalSlacker0079-Mar-12 7:24 
AnswerRe: Picture links broken? Pin
Sacha Barber9-Mar-12 7:42
Sacha Barber9-Mar-12 7:42 
QuestionVote of 5 Pin
Ganesan Senthilvel9-Mar-12 5:48
Ganesan Senthilvel9-Mar-12 5:48 
Good article
AnswerRe: Vote of 5 Pin
Sacha Barber9-Mar-12 7:35
Sacha Barber9-Mar-12 7:35 
QuestionWhy must WPF so cruefully complicated.... Pin
Bernhard Hiller9-Mar-12 0:28
Bernhard Hiller9-Mar-12 0:28 
AnswerRe: Why must WPF so cruefully complicated.... Pin
Sacha Barber9-Mar-12 0:52
Sacha Barber9-Mar-12 0:52 
QuestionThank you and some comments.... Pin
Christopher Camacho14-Oct-11 6:26
Christopher Camacho14-Oct-11 6:26 
AnswerRe: Thank you and some comments.... Pin
Sacha Barber9-Mar-12 0:51
Sacha Barber9-Mar-12 0:51 
GeneralThere is actually a keyboard interface in .NET4 Pin
Wieser Software Ltd23-Dec-10 0:22
Wieser Software Ltd23-Dec-10 0:22 
GeneralRe: There is actually a keyboard interface in .NET4 Pin
Sacha Barber23-Dec-10 1:56
Sacha Barber23-Dec-10 1:56 
GeneralRe: There is actually a keyboard interface in .NET4 Pin
Wieser Software Ltd23-Dec-10 2:16
Wieser Software Ltd23-Dec-10 2:16 
GeneralRe: There is actually a keyboard interface in .NET4 Pin
Sacha Barber23-Dec-10 3:16
Sacha Barber23-Dec-10 3:16 
GeneralMy vote of 5 Pin
prasad0213-Dec-10 6:53
prasad0213-Dec-10 6:53 
GeneralMy vote of 5 Pin
SteveTheThread7-Dec-10 22:10
SteveTheThread7-Dec-10 22:10 
GeneralRe: My vote of 5 Pin
Sacha Barber7-Dec-10 23:35
Sacha Barber7-Dec-10 23:35 
GeneralMy vote of 5 Pin
wizardzz7-Dec-10 11:33
wizardzz7-Dec-10 11:33 
QuestionBug? Pin
Nigel Stratton22-Nov-10 4:09
Nigel Stratton22-Nov-10 4:09 
AnswerRe: Bug? Pin
Sacha Barber2-Dec-10 18:12
Sacha Barber2-Dec-10 18:12 
AnswerRe: Bug? Pin
Sacha Barber2-Dec-10 18:15
Sacha Barber2-Dec-10 18:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.