Click here to Skip to main content
15,861,168 members
Articles / Desktop Programming / WPF

Extending the WPF Calendar Control

Rate me:
Please Sign up or sign in to vote.
4.84/5 (28 votes)
10 Sep 2010CPOL26 min read 153.8K   5K   65   40
This article shows how to create a custom control that extends an existing WPF control. It extends the WPF Calendar control by adding date highlighting.
FsCalendar_Screen_Shot.png

Introduction

I've got a real love-hate relationship with the WPF Calendar control. I'm glad to have the control, but it is missing some very basic features that should have been included in the control. One of those features is the ability to highlight a set of dates without having to select them. And that presents a good opportunity to show how to extend the WPF Calendar control, and WPF controls in general, particularly the more complex controls. That’s what this article does.

Changes from Prior Versions

Changes from Version 1.1: Version 2.0 is an extensive rewrite of the FS Calendar control, with several breaking changes. Why the changes? Once I started using the control in apps designed around the MVVM pattern, I got a rather nasty surprise. I had built the original calendar around a Dictionary<TKey, TValue> property to hold a set of dates and tool tips belonging to those dates. But I discovered rather quickly that Dictionary objects don’t databind well at all.

After trying in vain for a day or so to come up with a workaround, I was gobsmacked with the obvious solution: The tool tips collection doesn't need to be stored in a Dictionary; a simple array will do just fine, and it will databind very nicely. So, I wrote Version 2.0 of the FS Calendar control on that basis. It now works well with MVVM, but there is a tradeoff: Code written to work with Version 1.x will need to be changed. Here are the biggest changes:

  • The old HighlightedDates property, A Dictionary<DateTime, String>, has been replaced by a string array property called HighlightedDateText.
  • Version 1.x loaded all dates for all months into its dictionary, and the Calendar control retrieved highlighted dates for each month from the dictionary as the month was changed. Version 2.0 loads highlighted dates into its string array one month at a time, as the calendar month is changed.

In addition, the demo application has been changed. In version 1.x, it displayed a list of holidays. In version 2.0, it highlights odd-numbered dates, to simulate loading data from a database. All these changes are discussed below.

Earlier changes: Richard Deeming caught a bug in Version 1.0 that prevented the control from working properly if more than one calendar was placed in a window, page, or user control. And while fixing that bug, I found another one that caused the same problem. Both bugs were fixed in Version 1.1, and the changes made are discussed in the article.

Highlighting Dates in the WPF Calendar

In a calendar-based application, it is frequently useful to be able to highlight dates. For example, Outlook boldfaces any date that has any appointments. A social networking application might highlight birthdays, and so on—the applications for a highlighting facility are endless. Unfortunately, the WPF Calendar doesn't have that ability.

What I'd like is a HighlightedDates property that lets me specify dates to highlight. And while I am at it, I'd also like to be able to display a tool tip for each highlight with whatever contextual information I should decide to provide. For example, I am writing a note-taking application, and I would like the string to show the number of notes recorded for a particular date. I want the calendar to highlight any date that has one or more notes, and to display the number of notes in a tool tip.

Well, wanting is one thing and getting is quite another, so I set out to derive a custom control from the WPF Calendar that provides those capabilities. Fortunately, Charles Petzold did most of the heavy lifting in his June 2009 article in MSDN Magazine, Customizing the New WPF Calendar Controls. That article contains a ‘Red Letter Days’ example that can be readily adapted to do what I want. Thanks, Charles!

Petzold’s article does a great job of explaining the structure and workings of the WPF Calendar control, so I am going to skip those topics in this article. Instead, I am going to focus on the steps that are necessary to adapt Petzold’s work to fit a custom control, with a few enhanced capabilities. I recommend the Petzold article to those who are looking for more detail on the WPF Calendar control.

If you simply want to use the control, you don't need to wade through this article. Just take a quick look at the additional properties provided by the control, and you should be good to go. The added properties are located in a separate property category called Highlighting, so you can simply look at that category in VS 2010 to get a good idea of what the properties do. The bulk of the article explains how the modifications in the custom control are implemented, which will be helpful if you are learning how to perform WPF control modifications of your own.

Note that the control doesn't require you to use the MVVM pattern. The control is flexible enough to adapt to just about any architecture.

Step 1: Design

Here are the requirements for my control:

  1. The host app must be able to highlight dates.
  2. The host app must be able to display a unique tooltip for each highlighted date.
  3. The host app must be able to display highlighting without tool tips.
  4. The host application must be able to display the calendar without highlighting and tool tips.

Those requirements suggest that four properties needed to be added to the WPF Calendar:

  • HighlightedDateText: An array of 31 strings. If a string is null, its associated date is not highlighted. Otherwise, the string is displayed as a tool tip for that date, unless tool tips are disabled.
  • DateHighlightBrush: The color used to highlight dates.
  • ShowHighlightedDateText: Whether the text for highlighted dates will be displayed as tool tips.
  • ShowDateHighlighting: Whether highlighting will be shown. If highlighting is disabled, tool tips will not be shown.

In version 1.x of the FS Calendar, I used a Dictionary<DateTime, String> to store date highlighting strings. This approach seemed pretty reasonable, given the need to look up strings by date. That approach worked very well, so long as I was using procedural code to load the dictionary. But it didn't databind well at all, which meant it wouldn't work well with the MVVM pattern.

In light of the databinding problems, I decided that I didn't really need a dictionary object for the highlighted dates list--a string array will work just as well. Here's why: The days of the month are numbered, at most, from 1 to 31. That means I can use the day of the month as an index to fetch a string from an array of 31 elements. The string array is simpler than the dictionary, and it databinds better. So, I renamed the old property to HighlightedDateText and changed it to a string array.

Now for the highlighting itself. Ideally, I would like to be able to boldface a date, change its text color, and so on. Unfortunately, as we will see below, all that the calendar will allow me to do is highlight a date by changing its background color. So, the DateHighlightBrush property simply changes the color used for the background color.

The last two properties simply give the developer the option of highlighting without showing tool tips, or turning off highlighting and tool tips.

Step 2: Create Custom Control

This step is pretty straightforward. I create a Custom Control Library project in Visual Studio and rename the CustomControl1 class to the name of my custom control.

Step 3: Add Dependency Properties to Custom Control

The next step is to add properties to my control. I added them as dependency properties, to facilitate their use with the MVVM pattern. The properties are as described above.

The HighlightedDateText property is a 31-element string array. If a date is to be highlighted, the array index corresponding to the date will contain a text string. Assuming that the ShowHighlightedDateText property is set to true, the text associated with each date will be shown as a tool tip when the mouse is hovered over that date. Note that since arrays are indexed starting with zero, the index for each date is one less than the day number. For example, the first of the month is represented by HighlightedDateText[0], and so on.

The property declarations appear in FsCalendar.cs. The declarations are rather routine dependency property declarations, so I won't reproduce them here. Take a look at the CLR property wrappers, though. They show how to implement property categories--we use an attribute on the property:

C#
[Browsable(true)]
[Category("Highlighting")]
public Brush DateHighlightBrush
{
     ...
}

Note that there are several breaking changes in Version 2.0:

  • The HighlightedDateText property replaces the old HighlightedDates property;
  •  The old DateHighlightColor property has been renamed DateHighlightBrush;
  • The old ShowDateHighlights property has been renamed ShowHighlightedDateText; and
  • The old ShowHighlightTooltips property has been renamed ShowHighlightedDateText.

Step 4: Add a Value Converter

Now, this is where things start to get interesting. It will probably come as no surprise that we use a value converter in the process of adding highlighting to the WPF Calendar. But we are going to a multi-value converter, and we are going to use it in a way that is a bit different from what you might expect. In most projects, a value converter is little more than a code widget that performs a run-of-the-mill type conversion, such as converting a List to a delimited string. In our custom control, it does a bit more. To understand the value converter in the FsCalendar project, it helps to know about one of the quirks of the WPF Calendar control.

Each date has a data context: The WPF Calendar control sets the DataContext of every date in the month displayed. The DataContext is set to a DateTime object. Think about that for a minute—that is really an odd thing to do. But what it does is gives us access to each individual date in the Calendar control, albeit in a rather unusual way. We can insert an IConverter object between the date and its DataContext and manipulate the date through the IConverter.

Let me repeat that, because I missed it the first time I read over it in Petzold’s article. The WPF Calendar control itself sets a DataContext for each individual date in the control—it’s not a DataContext that we set. The control sets this DataContext to give control users—us—a way to access the date object. We can insert an IConverter to inject our own code into the Calendar. It’s hardly intuitive, but that’s how Petzold creates his ‘Red Letter Days’, and it is how we are going to wire up our highlighting properties.

The multi-value converter: The value converter needs access to several of the FsCalendar's properties. The most obvious solution would be to pass a reference to the current instance of the FsCalendar control in the binding's ConverterParameter property. Unfortunately, the ValueConverter property doesn't allow for that—it isn't a dependency property, so we can't use it to pass a RelativeReference to the current instance of the FsCalendar control. In Version 1.0 of the FsCalendar control, we created a static Parent property on the value converter to hold this reference, and we set the property in the FsCalendar constructor.

Unfortunately, that static property had a rather nasty side effect, which reader Richard Deeming picked up on in Version 1.0. Since the property was static, it meant that a single instance of the highlighted dates collection that the FsCalendar uses to set date highlighting was shared among all instances of the control. That's obviously not what we want—each instance of the control should have its own highlighted dates collection.

Richard also suggested a solution to the problem, which I incorporated into Version 1.1. The solution is to use an IMultiValueConverter, rather than the more usual IValueConverter, to perform the value conversion. I had seen the IMultiValueConverter before, but it had always been in the context of reading two different properties from a view model, massaging them somehow, and passing the result to the view. But here's a clever trick: you can use an IMultiValueConverter to pass a RelativeReference to the calling control in the value converter, just as we had wanted to do with the ConverterParameter property:

C#
<MultiBinding Converter="{StaticResource HighlightDate}">
    <MultiBinding.Bindings>
        <Binding />
        <Binding 
          RelativeSource="{RelativeSource FindAncestor, 
                          AncestorType={x:Type local:FsCalendar}}" />
    </MultiBinding.Bindings>
</MultiBinding>

I will talk more about the odd-looking first binding later. For now, simply note that we are passing two values into the converter, and the second one is a reference to the FsCalendar that is invoking it. Thanks, Richard, for spotting the problem and suggesting a great solution.

A dependency property trap: Before we get to how the value converter works, I want to mention another bug I discovered while implementing Richard Deeming's solution. After implementing Richard's solution, I was surprised to discover that I still had the same problem as before—one highlighted dates collection was being shared by all instances of the control! The problem wasn't with Richard's solution, but with my Version 1.0 code. A bit of investigation led me back to the DependencyProperty.Register() method.

In Version 1.0, I had initialized the old HighlightedDates property (changed to HighlightedDateText in Version 2.0) in the DependencyProperty.Register() method, like this:

C#
// The list of dates to be highlighted.
public static DependencyProperty HighlightedDatesProperty = DependencyProperty.Register
(
     "HighlightedDates",
     typeof (Dictionary<DateTime, String>),
     typeof (FsCalendar),
         new PropertyMetadata(new Dictionary<DateTime, String>())
);

Notice the new PropertyMetadata() parameter—the overload that I used initializes the property being registered.

Since I initialized the HighlightedDatesProperty in the static registration method, the property was initialized as a static property, resulting in the shared collection. Resolving the problem was simple—in Version 1.1, I changed the DependencyProperty.Register() call to an overload that takes an empty constructor for the new PropertyMetadata() parameter, so that the property isn't initialized in the DependencyProperty.Register() method. I carried the same approach over to Version 2.0, when initializing the replacement HighlightedDateText property:

C#
// The list of dates to be highlighted.
public static DependencyProperty HighlightedDateTextProperty = 
					DependencyProperty.Register
	(
		"HighlightedDateText",
		typeof (String[]),
		typeof (FsCalendar),
		new PropertyMetadata()
	);

Then I added an instance constructor to initialize the property as each instance of the FsCalendar is created. Here is Version 2.0:

C#
public FsCalendar()
{
    /* We initialize the HighlightedDateText property to an array of 31
     * strings, since 31 is the maximum number of days in any month. */

    // Initialize HighlightedDateText property
     this.HighlightedDateText = new string[31];
}

The moral of the story is to initialize dependency properties from an instance constructor, not from the DependencyProperty.Register() method, unless you want to initialize the property as being static.

How the IMultiValueConverter works: Okay, now we can get back to the value converter. The HighlightDateConverter has been completely rewritten for Version 2.0. It is generally based on the Petzold code, although Petzold uses an IValueConverter instead of an IMultiValueConverter, and the HighlightDateConverter does a bit more null-condition and design-time checking. Here is the new converter in full:

C#
using System;
using System.Windows.Data;

namespace FsControls
{
    public class HighlightDateConverter : IMultiValueConverter
    {
        #region IMultiValueConverter Members

        /// <summary>
        /// Gets a tool tip for a date passed in
        /// </summary>
        /// <param name="values">The array of values that the source bindings 
        /// in the System.Windows.Data.MultiBinding produces.</param>
        /// <param name="targetType">The type of the binding target property.</param>
        /// <param name="parameter">The converter parameter to use.</param>
        /// <param name="culture">The culture to use in the converter.</param>
        /// <returns>A string representing the tool tip for the date passed in.</returns>
        /// <remarks>
        /// 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 
        /// FsCalendar 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
            var targetDate = (DateTime)values[0];
            var parent = (FsCalendar) values[1];
            
            // Exit if highlighting turned off
            if (parent.ShowDateHighlighting == false) return null;

            /* At design-time, after we reset the calendar to the current date, 
             * we still may not have a HighlightedDateText array. 
             * If that's the case, we ignore the target date and exit. */

            // Exit if no HighlightedDateText array
            if (parent.HighlightedDateText == null) return null;

            /* The WPF calendar always displays six rows of dates, 
             * and it fills out those rows with dates from the preceding and 
             * following month. These 'gray' dates duplicate dates in the current 
             * month, so we ignore them. Their tool tips will appear in their 
             * own display months. */

            // Exit if target date not in the current display month
            if (!targetDate.IsSameMonthAs(parent.DisplayDate)) return null;

            // Get tool tip for date passed in
            string toolTip = null;
            var day = targetDate.Day;

            /* The HighlightedDateText array is indexed from zero, 
             * while the calendar is indexed from
             * one. So, we have to adjust the index between the array and the calendar. */

            // Get array index
            var n = day - 1;

            var dateIsHighlighted = !String.IsNullOrEmpty(parent.HighlightedDateText[n]);
            if (dateIsHighlighted) toolTip = parent.HighlightedDateText[n];

            // Set return value
            return toolTip;
        }

        /// <summary>
        /// Not used.
        /// </summary>
        public object[] ConvertBack(object value, Type[] targetTypes, 
		object parameter, System.Globalization.CultureInfo culture)
        {
            return new object[0];
        }

        #endregion
    }
}

The Calendar is going to pass a DateTime into the converter; it will appear as the values[0] argument in the Convert() method. The reference to the current instance of the FsCalendar control (the parent control) will be passed in as values[1].

The converter first checks to see if values were passed in. if either value[0] or value[1] is null, the converter exits and returns null to the calendar. If the converter gets past this point, it casts both values to their correct types. It now has a reference to its parent calendar, so it checks the calendar to see if date highlighting is turned off. If so, it exits and returns null to the calendar.

Next, the converter performs a couple of tests needed to ensure proper design-time performance. These tests are explained in the Convert() method comments. Then we get to an interesting problem. The WPF calendar always displays six rows of dates, and it fills out the calendar with leading dates from the previous month, and trailing dates into the next month. That means we will probably have multiple dates with the same index. For example, it is quite likely that the calendar will display the first of the month for two different months. That duplication could wreak havoc with our indexing.

The solution is to ignore any dates that are not actually in the month being displayed. The WPF calendar contains a property called DisplayDate, which is always a date in the month being displayed. That means we can use the DisplayDate to get the display month. From there, it's a simple matter to test if the date passed in by the calendar is in the display month. If it isn't, the converter returns null.

Note that we use an extension method, IsSameMonthAs(), to determine whether a date is in the display month. The extension method can be found in the DateTimeExtensions class, in the Utility folder of the FsCalendar project.

Finally, the converter is ready to get the highlight text for the date passed in. The converter uses the date to determine the appropriate index, then it fetches the corresponding element from the parent calendar's HighlightedDateText property. If the date is to be highlighted, the array element corresponding to the date will contain a string, which the calendar will ultimately display as a tool tip. If the date isn't highlighted, the element will be null.

So, what exactly does the parent control do with the value returned from the HighlightDateConverter? We will get to that next.

Step 5: Restyle the Calendar Control

From this point on, Version 2.0 is pretty much unchanged from Version 1.x. Our custom control has a Themes folder with a Generic.xaml markup file. The file and folder are ‘magic’, in the sense that they are required for a custom control. I discuss that subject in my article: Create a WPF Custom Control, Part 2.

For the next part of this discussion, you will need to understand the structure of a WPF Calendar control. It has several control templates that are arranged in a hierarchy. The Petzold article has a good discussion of the control’s structure, and I recommend you study that before proceeding.

Getting the styles you will need: Essentially, the WPF Calendar has a Style property that has a general control template for the control and several Style properties that manage the child styles. Our modifications will affect only the CalendarDayButton control template, which is contained in the CalendarDayButtonStyle. Nevertheless, we need two default styles for the WPF Calendar control:

  • The default style for the WPF Calendar, and
  • The default CalendarDayButtonStyle

As I mentioned above, the CalendarDayButton style contains the control template that we are going to modify. We need the Calendar style in order to point our custom control to our custom CalendarDayButton style. There is more on that below.

The CalendarDayButton object is discussed in the Petzold article. I got my copy of the templates using Expression Blend; you can find instructions for using Blend to copy a template in this MSDN article: Try it: Create a custom WPF control. If you don't have Expression Blend, there are several free tools available online for grabbing control templates.

Establishing a chain to the modified control template: WPF won't use our modified CalendarDayButton control template unless we lead it to the template step-by-step. Our starting point is the main Calendar control default style. We copy the entire style to our Generic.xaml file with only two changes:

  • First, we set the TargetType for the style to our custom control.
  • Then, we add a property setter for the CalendarDayButtonStyle property of the Calendar, and we set the property to point to our custom CalendarDayButton style.

With those changes, the Calendar style looks like this:

XML
<Style TargetType="{x:Type local:FsCalendar}">
    <Setter Property="CalendarDayButtonStyle" Value="{StaticResource
        FsCalendarDayButtonStyle}" />
    ...
</Style>

The rest of the Calendar style is unchanged from the default style.

These changes direct our custom FsCalendar to use our custom FsCalendarDayButtonStyle. The custom control will use the default control templates for other parts of the control (such as the CalendarItem and CalendarButton objects).

At this point, your Generic.xaml file should contain a Calendar style that has been modified as shown above, and an FsCalendarDayButtonStyle that is an exact copy of the default CalendarDayButtonStyle. Before you do any editing, it’s a good idea to verify that all is well up to this point. Create a simple WPF application that has a MainWindow, give it a reference to the custom control project, and add the custom control to the MainWindow. You should see a standard WPF Calendar control. If that’s the case, then you are ready to begin editing the CalendarDayButton control template.

Modifying the CalendarDayButton control template: Once you have the templates and have verified that they are working, you are ready to begin editing the FsCalendarDayButtonStyle to add highlighting features.

The CalendarDayButton control template contains a Grid control, which contains a VisualStateManager, followed by several rectangles and other objects. Here is what it looks like with the VisualStateManager collapsed:

Image 2

Adding a highlight rectangle: Let’s start by adding a rectangle for the highlight. We will insert it between the ‘TodayBackground’ rectangle and the ‘SelectedBackground’ rectangle. The CalendarDayButton already has a rectangle called ‘HighlightBackground’, which it uses in some of its animations, so we will name our rectangle ‘AccentBackground’:

XML
<Rectangle x:Name="TodayBackground" Fill="#FFAAAAAA" Opacity="0" RadiusY="1" RadiusX="1"/>

<!-- Added for FsCalendar -->
<Rectangle x:Name="AccentBackground"
           RadiusX="1" RadiusY="1"
          IsHitTestVisible="False"
          Fill="{Binding RelativeSource={RelativeSource AncestorType=local:FsCalendar}, 
    Path=DateHighlightBrush}" />
<!-- End addition -->

<Rectangle x:Name="SelectedBackground" Fill="#FFBADDE9" 
			Opacity="0" RadiusY="1" RadiusX="1"/>

Open MainWindow of your demo app again. At this point, all of the dates on the Calendar should be red, assuming you left the control’s DateHighlightBrush property at its default value.

Adding the value converter: Before we proceed any further, we need to add the value converter we discussed above. We will add a <ControlTemplate.Resources> section to the control template, just below the opening <ControlTemplate> tag, and add a declaration for the value converter to the new section:

XML
<ControlTemplate TargetType="{x:Type CalendarDayButton}">

    <!-- Added for FsCalendar -->
    <ControlTemplate.Resources>
        <local:HighlightDateConverter x:Key="HighlightDate" />
    </ControlTemplate.Resources>
    <!-- End addition -->

    <Grid>

Adding a tool tip: With the value converter in place, we are ready to add our tool tip. We add the tool tip to the opening <Grid> tag:

XML
<Grid x:Name="CalendarDayButtonGrid">
                         
    <!-- Added for FsCalendar -->
    <Grid.ToolTip>
          <MultiBinding Converter="{StaticResource HighlightDate}">
              <MultiBinding.Bindings>
                  <Binding />
                    <Binding RelativeSource="{RelativeSource FindAncestor, 
                                   AncestorType={x:Type local:FsCalendar}}" />
              </MultiBinding.Bindings>
          </MultiBinding>
    </Grid.ToolTip>
    <!-- End addition -->
    ...
</Grid>

The ToolTip multi-binding may look a bit odd to some, since the first binding doesn’t declare a binding target. Here’s what is going on:

First, let’s deal with the easy part: Why doesn’t the first binding declare a Path? Remember that the WPF calendar control gives each date a DataContext in the form of a DateTime object. Since the DataContext is a simple object (a DateTime), there is only one path possible. So, instead of something like this:

XML
<Binding Path=MyProperty />

we get the declaration shown above, with no Path property.

Now for the second question—how does the ToolTip declaration work, and why doesn’t it interfere with the date display? Here’s where it begins to get a little tricky. Remember that we are inside the CalendarDayButton control template. A different part of the date object gets the date to display, and that part can still access the DataContext as before. It is similar to the MVVM pattern, where several different properties can access the same DataContext. We are simply tapping into that DataContext from a different location, and using our value converter to get some text if the date appears on a highlighted dates list.

And that leaves us with one final task—creating triggers to show and hide the highlight and the tool tip. We add a new <ControlTemplate.Triggers> tag, just below the closing </Grid> tag:

XML
<!-- Added for FsCalendar -->
<ControlTemplate.Triggers>

    <!-- No tooltips if tooltips turned off -->
    <DataTrigger Binding="{Binding RelativeSource={RelativeSource 
                           AncestorType=local:FsCalendar}, Path=ShowHighlightToolTips}" 
                 Value="False">
        <Setter TargetName="CalendarDayButtonGrid" 
                Property="ToolTipService.IsEnabled" Value="False" />
    </DataTrigger>

    <!-- No highlighting if IValueConverter returned null -->
     <DataTrigger Value="{x:Null}">
          <DataTrigger.Binding>
               <MultiBinding Converter="{StaticResource HighlightDate}">
                    <MultiBinding.Bindings>
                         <Binding />
                         <Binding RelativeSource="{RelativeSource FindAncestor, 
                                            AncestorType={x:Type local:FsCalendar}}" />
                    </MultiBinding.Bindings>
               </MultiBinding>

          </DataTrigger.Binding>
        <Setter TargetName="AccentBackground" Property="Visibility" Value="Hidden" />
    </DataTrigger>

</ControlTemplate.Triggers>
<!-- End addition -->

The triggers are self-explanatory, and the multi-binding is the same as the one shown for the tool tip. The first trigger is bound to the ShowHighlightToolTips property of the custom control, and turns tool tips on or off according to the property setting. The second trigger is from the Petzold article. It turns off highlighting for a date that has no tool tip, by setting the Converter’s return value to null.

Refreshing the Calendar

In working with the calendar, I discovered pretty quickly that it has a notable quirk. It didn't always update its highlighting as expected. In some cases, the user had to change the month by clicking on one of the arrows, then change the calendar back. Quite frankly, I am not sure what causes this problem. I tried the usual methods of forcing a redraw (InvalidateArrange(), InvalidateLayout(), UpdateLayout(), and so on), but none of them had any effect. If any readers have feedback on the problem and a solution, I would appreciate it.

As a workaround, I have added a Refresh() method to the control. The method resets the DisplayDate property of the calendar to DateTime.MinValue, then immediately sets it back to the actual display date. This operation has the same effect as the user clicking to a different month, then clicking back. The difference is that it happens quickly enough to be transparent to the user. So, if the highlighting doesn't update as you expect, call the Refresh() method.

In my apps, I use the DisplayDate to trigger a load of the calendar's HighlightedDateText array. I bind the calendar's DisplayDate property to a view model DisplayDate property, then I watch the view model DisplayDate property for changes. When the DisplayDate changes to a different month, as it will when a user clicks on one of the calendar's arrows, the view model loads the new month's data into the HighlightedDateText property.

It's a good approach, but it has a side-effect. Invoking the Refresh() method triggers two attempts to load the array; once for January 0001 (the month for DateTime.MinValue), and a second time for the month being refreshed. If you use the same approach to load data into the calendar, you can improve performance by watching for DateTime.MinValue and ignoring that particular change. You can see the code to do this in the following section of this article.

The Demo App

The demo app, which has been completely rewritten for Version 2.0, shows how to utilize the control. In Version 1.x, I used a dictionary to hold the list of dates and associated strings. The advantage to this approach was that it allowed the developer to load data all at once, or month by month. For example, I could load five years of data when an app was initialized, or I could load each month's data as the calendar was changed to a new month. The switch to a string array eliminated the all-in-one approach, which the Version 1.x demo used. The Version 2.0 demo illustrates the month-at-a-time approach using the MVVM pattern.

In the new demo, I wanted to show how to get data one month at a time using the MVVM pattern, but I didn't want to burden the demo with database code. So the demo simply highlights odd-numbered days and sets each highlighted date's highlight text to its long date string. All of the work is done in the view model, although in a production app I would probably move the heavy lifting out to service methods in a business layer.

The View and the View Model are instantiated and connected in an override to the App.OnStartup() method in App.xaml.cs:

C#
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        var mainWindow = new MainWindow();
        var mainWindowViewModel = new MainWindowViewModel();
        mainWindow.DataContext = mainWindowViewModel;
        mainWindow.Show();
    }
}

There is really only one trick to the View Model.  It subscribes to its own PropertyChanged event in its Initialize() method, which is called by the View Model's constructor:

C#
private void Initialize()
{
    ...

    // Subscribe to PropertyChanged event
    this.PropertyChanged += OnPropertyChanged;
}

When the event is raised for the DisplayDate property, the View Model's OnPropertyChanged() method handles the event:

C#
private void OnPropertyChanged
(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    // Ignore properties other than DisplayDate
    if (e.PropertyName != "DisplayDate") return;

    // Ignore change if date is DateTime.MinValue
    if (p_DisplayDate == DateTime.MinValue) return;

    // Ignore change if month is the same
    if (p_DisplayDate.IsSameMonthAs(m_OldDisplayDate)) return;

    // Populate month
    this.SetMonthHighlighting();

    // Update OldDisplayDate
    m_OldDisplayDate = p_DisplayDate;
}

The event handler first tests the new display date to see if it is DateTime.MinValue. If so, then it means the control is refreshing itself, and the event handler ignores the date. Next, the event handler tests whether the date falls in the same month as the previous display date, which is stored in a member variable. It uses the IsSameMonthAs() extension method discussed earlier, which it finds in a Utility folder in the demo project. If both dates fall in the same month, the event handler ignores the new date and exits. If the new display date falls in a different month, the event handler calls the View Model's SetMonthHighlighting() method to load the new month's highlighted date text into the View Model's HighlightedDateText property:

C#
private void SetMonthHighlighting()
{
    var displayMonth = this.DisplayDate.Month;
    var displayYear = this.DisplayDate.Year;

    // Get the last day of the display month
    var month = this.DisplayDate.Month;
    var year = this.DisplayDate.Year;
    var lastDayOfMonth = DateTime.DaysInMonth(year, month);

    // Set the highlighted date text
    for (var i = 0; i < 31; i++)
    {
        // First set this array element to null
        p_HighlightedDateText[i] = null;

        /* This demo simply highlights odd dates. So, if the array element represents 
         * an even date, we leave the element at its null setting and skip to the next 
         * increment of the loop. Note that the array is indexed from zero, while a 
         * calendar is indexed from one. That means odd-numbered elements represent 
         * even-numbered dates. So, if the index is odd, we skip. */

        // If index is odd, skip to next
        if (i % 2 == 1) continue;

        /* An element may be out of range for the current month. For example, element
         * 30 would represent the 31st, which would be out of range for a month that 
         * has only 30 days. If that's the case for the current element, we leave it
         * set to null and skip to the next increment of the loop. */

        // If element is out of range, skip to next
        if (i >= lastDayOfMonth) continue;

        /* Since the array is indexed from zero, and a calendar is indexed from one, 
         * we have to add one to the array index to get the calendar day to which it 
         * corresponds. All we do in this demo is put the Long Date String is the
         * HighlightedDateText array. */

        // Set highlight date text
        var targetDate = new DateTime(displayYear, displayMonth, i + 1);
        p_HighlightedDateText[i] = targetDate.ToLongDateString();
        
        // Refresh the calendar
        this.RequestRefresh();
    }
}

Note that if you want to show highlighting without a tool tip display, simply set the ShowHighlightedDateText property to false. However, you still need to insert some sort of string value into an array element for each highlighted date. Since the tool tip won't be displayed, you can set the string to anything you want, so long as it isn’t null or empty.

Note also that we refresh the calendar after updating the HighlightedDateText property. We do so by calling the view model's RequestRefresh() method, which raises a RefreshRequested event, to which the view subscribes. We take this roundabout route, instead of invoking a refresh method on the view, because of the MVVM pattern.

MVVM is a very flexible pattern, and there are several different ways in which it can be implemented. I prefer to implement the pattern so that a view is dependent on its view model, but not vice-versa--the view model is independent of the view. This keeps the dependencies running in one direction, and I find that it simplifies maintenance considerably, because the view model doesn't care what view is connected to it. My implementation of MVVM is strict enough in this regard that the view model does not even contain an injected reference to a view, or even a view interface.

That approach minimimizes coupling between a view and its view model, which makes it much easier to unbolt a view from the application and bolt on something completely different. But it also means that a view model can have no knowledge of the view that uses it. Given that limitation, the only way that the view can communicate with the view is by raising an event, as we do here. We will see how the view consumes the event shortly.

The View (MainWindow.xaml) is pretty straightforward. It contains an FsCalendar control, which is bound to the View Model as follows:

XML
<FsControls:FsCalendar x:Name="DemoCalendar" 
                         DisplayDate="{Binding Path=DisplayDate, 
			Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                         HighlightedDateText="{Binding Path=HighlightedDateText, 
			Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                         ... />

The code-behind for the View (MainWindow.xaml.cs) contains two event handlers that work together to process the RefreshRequested event published by the View Model. The MainWindow.OnDataContextChanged event is bound in XAML to an OnDataContextChanged event handler, which subscribes to the MainWindowViewModel.RefreshRequested event. When the RefreshRequested event fires, this second event handler invokes the Refresh() method on the calendar:

C#
#region Event Handlers

/// <summary>
/// Subscribes to the view model's RefreshRequested event.
/// </summary>
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    var mainWindowViewModel = (MainWindowViewModel) DataContext;
    mainWindowViewModel.RefreshRequested += OnRefreshRequested;
}

/// <summary>
/// Refreshes the calendar.
/// </summary>
private void OnRefreshRequested(object sender, EventArgs e)
{
    this.DemoCalendar.Refresh();
}

#endregion

Now, I know some MVVM purists are going to jump up and down about these event handlers. One dialect of MVVM requires that everything be done in XAML, with no code-behind. To those who prefer that flavor of MVVM, I respect and salute you. But there are some things that simply can't be done in XAML. In those cases, I follow the rule that says code-behind is okay, so long as it only addresses concerns of the View. Problems arise only when back-end concerns find their way into code-behind. Of course, if these event handlers can be replaced by XAML, I am open to that, and I welcome your comments.

Obviously, the View and View Model are intentionally simplistic in this demo. A production app will use more properties of the calendar, such as the SelectedDate property, to show data for a date when it is selected by the user. But there should be enough here to show you how to wire the control up to a view model.

Conclusion

In this article, I have shown how to extend a complex WPF user control, particularly when one needs to modify a nested control template buried deep within the control. The techniques described in this article can be applied to WPF controls other than the Calendar control. The key is to understand how the particular control is structured, and how to establish the chain from the topmost control template to the control templates you need to modify.

I am always looking for peer review of these articles, so I invite you to post your comments, questions, and suggestions below this article. Thanks for your input!

History

  • 20 August, 2010: Version 1.0. Initial version
  • 29 August, 2010: Version 1.1. Removed Parent property from value converter; replaced with IMultiValueConverter
  • 9 September 2010: Version 2.0. Replaced Dictionary<DateTime, String> with a string array; added Refresh() method; rewrote demo

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) Foresight Systems
United States United States
David Veeneman is a financial planner and software developer. He is the author of "The Fortune in Your Future" (McGraw-Hill 1998). His company, Foresight Systems, develops planning and financial software.

Comments and Discussions

 
QuestionMessage Removed Pin
9-Sep-20 1:04
Member 149319419-Sep-20 1:04 
QuestionQuestion about Theme Pin
Max Zaikin22-Feb-18 1:55
Max Zaikin22-Feb-18 1:55 
BugMinor bug Pin
_Noctis_26-Nov-14 15:52
professional_Noctis_26-Nov-14 15:52 
GeneralRe: Minor bug Pin
Member 121126114-Nov-15 2:46
Member 121126114-Nov-15 2:46 
GeneralRe: Minor bug Pin
_Noctis_4-Nov-15 11:00
professional_Noctis_4-Nov-15 11:00 
GeneralRe: Minor bug Pin
Member 1038187623-Oct-17 15:13
Member 1038187623-Oct-17 15:13 
GeneralRe: Minor bug Pin
_Noctis_24-Oct-17 0:17
professional_Noctis_24-Oct-17 0:17 
SuggestionPost project screenshots as well. Pin
Hem Pokhrel26-May-14 20:09
Hem Pokhrel26-May-14 20:09 
QuestionDifferent Colors for specific Dates Pin
Cloud92521-Nov-12 21:21
Cloud92521-Nov-12 21:21 
GeneralMy vote of 5 Pin
deriric25-Jul-12 5:54
deriric25-Jul-12 5:54 
QuestionWPF calendar control Pin
Fistum Mekuria9-Apr-12 6:43
Fistum Mekuria9-Apr-12 6:43 
SuggestionRefreshing Without Calling Refresh() Pin
Heinrich Strauss29-Feb-12 15:36
Heinrich Strauss29-Feb-12 15:36 
QuestionProblems when month changes Pin
Member 850331222-Dec-11 5:22
Member 850331222-Dec-11 5:22 
BugNot update after DisplayDate change Pin
Luva455-Dec-11 13:40
Luva455-Dec-11 13:40 
GeneralAn other view Pin
serhhio8-Jun-11 7:30
serhhio8-Jun-11 7:30 
GeneralRe: An other view Pin
David Veeneman8-Jun-11 9:57
David Veeneman8-Jun-11 9:57 
Generalgood stuff, but... [modified] Pin
tronix0126-Jan-11 5:31
tronix0126-Jan-11 5:31 
GeneralHey man I just made some use of this. [modified] Pin
Sacha Barber12-Nov-10 3:45
Sacha Barber12-Nov-10 3:45 
GeneralRe: Hey man I just made some use of this. Pin
David Veeneman12-Nov-10 4:34
David Veeneman12-Nov-10 4:34 
GeneralRe: Hey man I just made some use of this. Pin
Sacha Barber12-Nov-10 5:13
Sacha Barber12-Nov-10 5:13 
QuestionNice this is exactly what I was looking for. Pin
nportelli4-Oct-10 8:52
nportelli4-Oct-10 8:52 
AnswerRe: Nice this is exactly what I was looking for. Pin
Sacha Barber12-Nov-10 5:14
Sacha Barber12-Nov-10 5:14 
GeneralRefresh Calendar and foreground color Pin
ovivela23-Sep-10 4:42
ovivela23-Sep-10 4:42 
GeneralRe: Refresh Calendar and foreground color Pin
David Veeneman23-Sep-10 4:48
David Veeneman23-Sep-10 4:48 
GeneralRe: Refresh Calendar and foreground color Pin
Chris D Early25-Oct-12 7:51
Chris D Early25-Oct-12 7:51 

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.