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:
- The host app must be able to highlight dates.
- The host app must be able to display a unique tooltip for each highlighted date.
- The host app must be able to display highlighting without tool tips.
- 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:
[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 ShowHighlightedDate
Text.
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:
<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:
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
:
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:
public FsCalendar()
{
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:
using System;
using System.Windows.Data;
namespace FsControls
{
public class HighlightDateConverter : IMultiValueConverter
{
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if ((values[0] == null) || (values[1] == null)) return null;
var targetDate = (DateTime)values[0];
var parent = (FsCalendar) values[1];
if (parent.ShowDateHighlighting == false) return null;
if (parent.HighlightedDateText == null) return null;
if (!targetDate.IsSameMonthAs(parent.DisplayDate)) return null;
string toolTip = null;
var day = targetDate.Day;
var n = day - 1;
var dateIsHighlighted = !String.IsNullOrEmpty(parent.HighlightedDateText[n]);
if (dateIsHighlighted) toolTip = parent.HighlightedDateText[n];
return toolTip;
}
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:
<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:
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
’:
<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=local:FsCalendar},
Path=DateHighlightBrush}" />
-->
<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:
<ControlTemplate TargetType="{x:Type CalendarDayButton}">
-->
<ControlTemplate.Resources>
<local:HighlightDateConverter x:Key="HighlightDate" />
</ControlTemplate.Resources>
-->
<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:
<Grid x:Name="CalendarDayButtonGrid">
-->
<Grid.ToolTip>
<MultiBinding Converter="{StaticResource HighlightDate}">
<MultiBinding.Bindings>
<Binding />
<Binding RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type local:FsCalendar}}" />
</MultiBinding.Bindings>
</MultiBinding>
</Grid.ToolTip>
-->
...
</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:
<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:
<!---->
<ControlTemplate.Triggers>
<!---->
<DataTrigger Binding="{Binding RelativeSource={RelativeSource
AncestorType=local:FsCalendar}, Path=ShowHighlightToolTips}"
Value="False">
<Setter TargetName="CalendarDayButtonGrid"
Property="ToolTipService.IsEnabled" Value="False" />
</DataTrigger>
<!---->
<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>
<!---->
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:
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:
private void Initialize()
{
...
this.PropertyChanged += OnPropertyChanged;
}
When the event is raised for the DisplayDate
property, the View Model's OnPropertyChanged()
method handles the event:
private void OnPropertyChanged
(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName != "DisplayDate") return;
if (p_DisplayDate == DateTime.MinValue) return;
if (p_DisplayDate.IsSameMonthAs(m_OldDisplayDate)) return;
this.SetMonthHighlighting();
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:
private void SetMonthHighlighting()
{
var displayMonth = this.DisplayDate.Month;
var displayYear = this.DisplayDate.Year;
var month = this.DisplayDate.Month;
var year = this.DisplayDate.Year;
var lastDayOfMonth = DateTime.DaysInMonth(year, month);
for (var i = 0; i < 31; i++)
{
p_HighlightedDateText[i] = null;
if (i % 2 == 1) continue;
if (i >= lastDayOfMonth) continue;
var targetDate = new DateTime(displayYear, displayMonth, i + 1);
p_HighlightedDateText[i] = targetDate.ToLongDateString();
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:
<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:
#region Event Handlers
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var mainWindowViewModel = (MainWindowViewModel) DataContext;
mainWindowViewModel.RefreshRequested += OnRefreshRequested;
}
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