June 2009
Volume 24 Number 06
Foundations - Customizing the New WPF Calendar Controls
By Charles Petzold | June 2009
Contents
Calendar: A Brief Orientation
Styling the Calendar
Setting a New Calendar Template
Replacing the CalendarItem Template
The Powerful CalendarDayButton Template
Deriving from Calendar
The DatePicker Control
When Microsoft released Windows Presentation Foundation (WPF) in 2006, everyone agreed it was missing a couple of pieces, most noticeably calendar controls. It seemed like an odd omission—after all, Windows Forms had MonthCalendar and DateTimePicker controls, so why not just port those to WPF?
But porting a control to WPF isn't a simple matter of just changing property and event names. WPF controls must be "lookless." The visual appearance of the control must be separated into a visual tree of other elements and controls. This default template must be structured and documented in such a way to be completely replaceable. In this way developers and designers can customize controls to give them a whole new appearance.
Designing a control with a replaceable template can be intimidating, as I discovered when I attempted to design a MonthCalendar control for my article "Templates for Uncommon Controls" in the January 2008 issue. You want to make the control as generalized as possible to be amenable to a variety of different appearances, yet without making the interface between the code and template inordinately complex. Calendars introduce another level of complexity because they are heavily reliant on cultural conventions, and it's not obvious to what extent a custom template should be allowed to violate those conventions.
That's possibly why WPF calendar controls took so long. But late last year Microsoft released Calendar and DatePicker controls (with a few subsidiary classes) in a WPF Toolkit. (A new DataGrid is also included, but I won't be discussing that here.) These controls are very much compatible with the same controls in Silverlight, and they will probably end up in the next release of WPF.
For the remainder of this article, I'll assume you have downloaded the WPF Toolkit source code and binaries and run the installer to register WPFToolkit.dll. Once this DLL is registered, you can use it in a WPF project in Visual Studio by right-clicking the References tag in the project, selecting Add Reference, and then selecting the .NET tab. The WPF Toolkit DLL will be found toward the bottom of the list.
Calendar: A Brief Orientation
The Calendar and DatePicker classes (along with some supporting enumerations and delegates) have a namespace of Microsoft.Windows.Controls. Some subsidiary classes, mostly notably CalendarItem, CalendarButton, CalendarDayButton, and DatePickerTextBox, are in the Microsoft.Windows.Controls.Primitives namespace. These are not the customary namespaces for WPF controls, so you'll probably need to add new using directives in your C# code and construct the proper XML namespace declarations in XAML files. Classes related to the new Visual State Manager (which I'll discuss later in this article) have a more normal namespace of System.Windows.
The Calendar class derives from Control. If you simply instantiate Calendar, you get the view shown on the left in Figure 1. Only one month is displayed—never multiple months as the Windows Forms MonthCalendar does. The month always includes six weeks, even if only four or five are required. Click the header (the part at the top indicating the month and year) and you'll switch to the year view in the center. You can click one of the months to return to a view of that month, or click the header again to get the decade view shown at the right. Click one of the years to return to the year view.
Figure 1 The Three Calendar Display Modes)
These three views correspond to the DisplayMode property of Calendar, which is set to one of the three members of the CalendarMode enumeration: Month, Year, or Decade.
The two buttons on each side of the header navigate forward or backward by month, by year, or by decade.
The DisplayDate property is of type DateTime and indicates which month, year, or decade is displayed. DisplayDateStart and DisplayDateEnd properties are of type nullable DateTime and restrict the navigation of the calendar to a particular range.
The IsTodayHighlighted property is true by default and causes a dark square to be drawn behind today's date. Dates can also be selected. Set the SelectionMode property to a member of the CalendarSelectionMode enumeration: SingleDate (the default), SingleRange, MultipleRange, or None. For the SingleDate mode, the SelectedDate property (of type nullable DateTime) indicates the selection; otherwise, the SelectedDates property is of type SelectedDatesCollection, which derives from ObservableCollection of type DateTime.
The BlackoutDates property lets you inhibit selection of various ranges of dates. The property is of type CalendarBlackoutDateCollection, which derives from ObservableCollection of type CalendarDateRange, a class that defines Start and End properties of type DateTime.
The FirstDayOfWeek property is of type DayOfWeek; the default is usually Sunday, but for some locales (most notably France) the default is Monday.
Calendar defines four events: DisplayModeChanged, DisplayDateChanged, SelectionModeChanged, and SelectedDatesChanged.
Styling the Calendar
The remaining public properties of the Calendar class are CalendarItemStyle, CalendarButtonStyle, and CalendarDayButtonStyle, all of type Style, but to understand these properties you need to know how the Calendar control is built and structured, and for that knowledge it helps to examine some XAML.
All the source code for the Calendar control is downloadable from the CodePlex site, but if you're interested in customizing the control, the most important file is undoubtedly Generic.xaml, located in the subdirectory Toolkit-release\Calendar\Themes. The Generic.xaml file contains the default styles (including templates) for the Calendar control itself and the other controls that make up the Calendar control. If you'll be templating Calendar, you'll want to study Generic.xaml very closely.
Although the Calendar class defines all the properties and events for the control, and also performs some of the user input processing, the Calendar template in Generic.xaml reveals that Calendar consists only of an invisible StackPanel and a CalendarItem control.
The CalendarItem class also derives from Control and encompasses the entire visual appearance of the control. This includes the three buttons at the top (which I'll refer to collectively as the navigation buttons). It also includes two grids. One grid shows the days of the month (including the headers for the days of the week). The other grid shows either 12 months or 12 years. At any time only one of these two grids is visible.
CalendarItem is also responsible for populating those two grids. When showing the days of the month, it creates objects of type CalendarDayButton; the 12 months or years are of type CalendarButton. Both classes derive from Button.
The Generic.xaml includes the default styles and templates for Calendar, CalendarItem, CalendarButton, and CalendarDayButton. For these latter three items, you can set new styles (including templates) by setting the CalendarItemStyle, CalendarButtonStyle, or CalendarDayButtonStyle properties defined by Calendar, either directly or through the Style property of Calendar.
Setting a Style for Calendar itself is useful for Calendar-specific properties (such as DisplayMode or SelectionMode) or properties inherited by Calendar (such as HorizontalAlignment or Margin), but I want to focus on more problematic classes of properties: those relating to fonts and brushes.
The Calendar control is very precisely built from its subsidiary controls, and if you start messing around with fonts, the control can quickly become visually lopsided. For example, you can set the FontFamily property on Calendar and it will affect all text except the day-of-week headings. (The FontFamily for those headings is hardcoded in a DateTemplate inside the default template for CalendarItem.) However, if the text of the days of the month becomes a little smaller as a result, the whole month will seem to be shifted to the left.
Setting the FontSize property on Calendar has no effect. If you truly want to change the FontSize you'll have to change it in the CalendarButton style, the CalendarDayButton style, and two places inside the CalendarItem template. Forget about it. If you want to make the Calendar control a little larger or smaller than normal, don't do it with FontSize. I suggest defining a ScaleTransform and setting that to the Calendar's LayoutTransform property, or perhaps putting the Calendar in a Viewbox.
The colors that Calendar uses for marking the current day, selected dates, and blackout dates are hardcoded in the various templates. The only brushes you can easily change are the Background property of the Calendar control and the BorderBrush that surrounds the control. These are both set to linear gradient brushes in the default style of Calendar and then passed to CalendarItem via template bindings. If you want to replace the default Background brush, which I'll demonstrate later in this article, keep in mind that it's responsible for the different background shade behind the navigation buttons that you can see in Figure 1.
Setting a New Calendar Template
The first step in replacing a template for a control is looking at the class definition, either in documentation or the actual source code (if available). There you will see a collection of TemplatePart attributes. These TemplatePart attributes indicate named controls or elements that your new template must have to be entirely functional. For the Calendar control, a new template must have at least two parts: a CalendarItem with the name PART_CalendarItem and a Panel with the name PART_Root. (In the default Calendar template, the PART_Root Panel is a StackPanel.)
It's unlikely that you'll be replacing the template for the Calendar, but I shouldn't try to second guess your needs. Perhaps you'd like to add something to the visual tree that makes up Calendar, such as a scrollable ItemsControl that displays all the selected dates.
Such an application is illustrated by the SelectedDateLister project included with the downloadable source code for this column. Figure 2 shows an excerpt from the MainWindow.xaml file of that project with a new template for Calendar, and Figure 3 shows what it looks like.
Figure 2 A New Template for Calendar
<ControlTemplate TargetType="toolkit:Calendar"> <StackPanel Name="PART_Root" Orientation="Horizontal"> <primitives:CalendarItem Name="PART_CalendarItem" Style="{TemplateBinding CalendarItemStyle}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" VerticalAlignment="Center" /> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Margin="4 4 0 4"> <ScrollViewer VerticalScrollBarVisibility="Auto" Height="{Binding ElementName=PART_CalendarItem, Path=ActualHeight}" Width="100"> <ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=toolkit:Calendar}, Path=SelectedDates}" /> </ScrollViewer> </Border> </StackPanel> </ControlTemplate>
Figure 3 Calendar with Selected Dates List
Notice the XML namespace prefix of primitives needed to reference the CalendarItem control. In the root element of the XAML file, that prefix is associated with the .NET namespace of Microsoft.Windows.Controls.Primitives and the WPFToolkit assembly. Similarly, the prefix toolkit is associated with the Microsoft.Windows.Controls namespace in the same assembly. The Calendar is simply instantiated in the XAML like so:
<toolkit:Calendar />
The style containing the new template is applied implicitly.
Notice also in Figure 2 the use of a Binding with RelativeSource to connect the ItemsSource property of the ItemsControl with the SelectedDates property of the Calendar. TemplateBinding can't be used for this job because SelectedDates is not backed with a dependency property.
How did I know what properties of CalendarItem I should set with TemplateBinding? I looked at the default Calendar template in Generic.xaml. In fact, I not only looked at it: I copy-and-pasted it right into my source code. Copy-and-paste is, I'm afraid, the best way to make modifications to existing templates.
Replacing the CalendarItem Template
To have more control over the internal appearance of the Calendar control, it's necessary to replace the template for CalendarItem. The default template is a substantial chunk of XAML—almost 250 lines in the Generic.xaml file—and it includes three embedded templates for the navigation buttons that appear at the top.
Experienced WPF programmers who haven't yet delved into Silverlight will be surprised to see something unfamiliar in the three embedded button templates: references to classes named VisualStateManager, VisualStateGroup, and VisualState. These classes are major components of the Visual State Manager that is already in Silverlight and expected to become part of WPF in the next major release. (The source code for this preliminary Visual State Manager is included in the WPF Toolkit.)
The Visual State Manager is intended to replace many of the triggers currently used in templates. Template triggers are customarily based on values of certain properties, such as IsEnabled, IsMouseOver, and IsSelected. The Visual State Manager is instead based around a more structured concept of visual states, with names such as Disabled, MouseOver, and Selected. At first glance, these visual states seem to have a one-to-one correspondence with triggers, but that's not really so. In practical use, triggers can result in different visual appearances depending on the order in which they appear, and often it becomes clumsy to define multiple triggers when triggers need to work in conjunction with each other. The alternative identification of visual states makes the programmer's intention much more obvious. The association between property settings and visual states occurs in code.
To reference the Visual State Manager classes in XAML, you'll need to define an additional XML prefix (vsm, for example) associated with the System.Windows namespace in the WPFToolkit assembly.
A custom template for CalendarItem should have eight named parts, as shown in Figure 4.
Figure 4 Named Parts for the CalendarItem Template
Part | Type |
PART_Root | FrameworkElement |
PART_HeaderButton | Button |
PART_PreviousButton | Button |
PART_NextButton | Button |
DayTitleTemplate | Key name of DataTemplate |
PART_MonthView | Grid |
PART_YearView | Grid |
PART_DisabledVisual | FrameworkElement |
Notice that one of these names is actually a resource key name. The Resources section of the CalendarItem template should include a DataTemplate with a key name of "DayTitleTemplate" that contains a TextBlock element with the Text property set like so:
Text="{Binding}"
This DateTemplate is used to display the headings with the days of the week. The CalendarDayButton items used for these headers has its Content set to null, but its DataContext set to the text header (Su, Mo, Tu, and so forth), hence the strange binding. (I'll have more to say on the DataContext property of the CalendarDayButton later.)
The default visual tree of the CalendarItem template consists of several nested grids. An outer single-cell Grid is named PART_Root. This Grid contains a Border and a Rectangle named PART_DisabledVisual intended to provide a semi-opaque covering when the control is disabled.
The Border contains another Grid, this one with three columns and two rows providing the main structure to the control. The three named navigation buttons occupy the top row. The bottom row contains two grids (PART_MonthView and PART_YearView) spanning the three columns.
The Grid named PART_MonthView is expected to have seven columns (for the seven days of the week) and seven rows (one for the day-of-week headings and six for the days of the month), while PART_YearView is expected to have four columns and three rows (for 12 months of a year or 12 years in a "decade" view). When the code part of CalenderItem creates the CalendarDayButton and CalendarButton children to populate these grids, it explicitly sets the Grid.Row and Grid.Column attached properties to each child.
Figure 5 A New Bare-Bones
CalendarItem Template
For this reason, it is unlikely that a custom CalendarItem template will differ significantly from the existing layout. If you want to take a shot at it, the MainWindow.xaml file in the BareBonesCalendarItem project has pretty much the minimum markup you need to get something complete and functional. The resultant visual is shown in Figure 5. Just to make it a little different, I built the overall structure from a DockPanel, so that the buttons for previous-month and next-month are on the sides. I didn't apply templates to these navigation buttons, which is why they look like actual buttons. Even with these simplifications, the template is almost 80 lines in length. It looks a little odd, but it works.
A safer and more rational approach to defining a new CalendarItem template is to simply copy the default template for CalendarItem from Generic.xaml into your own XAML file, and then modify only certain parts of it.
That's what I did in the ColorfulCalendarItem project. The MainWindow.xaml file defines an XML namespace prefix of local rather than toolkit for referencing the Calendar class to be consistent with the namespace declarations in Generic.xaml.
In the CalendarItem template I added a new Background brush for the outer Border:
<LinearGradientBrush StartPoint="0 0" EndPoint="0 1"> <GradientStop Offset="0" Color="#FFFFC0" /> <GradientStop Offset="0.5" Color="#FFE0B0" /> <GradientStop Offset="1" Color="#FFD0A8" /> </LinearGradientBrush>
I also added a new Border around the ContentPresenter for the center header button and gave that a background as well:
<Border Padding="12 0" CornerRadius="6"> <Border.Background> <LinearGradientBrush StartPoint="0 0" EndPoint="0 1"> <GradientStop Offset="0" Color="#FFC4A0" /> <GradientStop Offset="1" Color="#FF9450" /> </LinearGradientBrush> </Border.Background>
The result is shown in Figure 6.
Figure 6 New Colors Defined
in the CalendarItem Template
The Powerful CalendarDayButton Template
The CalendarItem template contains embedded templates for the three navigation buttons displayed at the top of the calendar, but the template has no references to the CalendarButton and CalendarDayButton objects that populate the lower grids. These buttons are instantiated in code, as you can note by examining the CalendarItem.cs file. However, you can define new templates for these buttons by setting a Style object to the CalendarButtonStyle and CalendarDayButtonStyle properties of Calendar.
A custom template for CalendarDayButton is potentially very powerful. Here you can display additional information for each day beyond the customary two-digit number between 1 and 31. At first it doesn't seem as if you can do this in an intelligent manner, because anything that you add to the content of the CalendarDayButton would probably need to depend on the exact date represented by that button, and that information doesn't seem to be readily available.
Don't fret. Although the Content of the CalendarDayButton is set to a simple text string such as 24, the DataContext property of each CalendarDayButton is set to the actual DateTime object for that date, including the correct month and year. (This feature also helps distinguish the CalendarDayButton objects used for the day-of-week headings; those buttons have a null Content but a DataContext set to the text strings Su, Mo, and so forth. This is why a DataTemplate is required in CalendarItem for these headings.)
The availability of a DateTime object opens up a whole area of possible enhancements that you can implement simply by referencing some custom class in a new CalendarDayButton template. Because the default CalendarDayButton template is about 120 lines of XAML, you probably won't be writing such a template from scratch. As with the CalendarItem template, you'll probably want to copy the default CalendarDayButton template from Generic.xaml into your own project, and then modify it.
One possibility is to display holidays and other important days in a ToolTip displayed when you pass the mouse pointer over each day, as illustrated by the project RedLetterDays. It seemed reasonable to implement this feature as a binding converter: in effect, the converter converts a DateTime object into a String object.
Figure 7 shows the important parts of this binding converter. The constructor sets items for dates between March (when I'm writing this article) and June (when it will be published). In a real program, it would probably access these dates and text strings from a file.
Figure 7 Most of a Binding Converter for RedLetterDays
public class RedLetterDayConverter: IValueConverter { static Dictionary < DateTime, string > dict = new Dictionary < DateTime, string > (); static RedLetterDayConverter() { dict.Add(new DateTime(2009, 3, 17), "St. Patrick's Day"); dict.Add(new DateTime(2009, 3, 20), "First day of spring"); dict.Add(new DateTime(2009, 4, 1), "April Fools"); dict.Add(new DateTime(2009, 4, 22), "Earth Day"); dict.Add(new DateTime(2009, 5, 1), "May Day"); dict.Add(new DateTime(2009, 5, 10), "Mother's Day"); dict.Add(new DateTime(2009, 6, 21), "First Day of Summer"); } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { string text; if (!dict.TryGetValue((DateTime) value, out text)) text = null; return text; } ... }
The MainWindow.xaml file contains the template for CalendarDayButton with just a few additions. The first is the binding converter stored as a resource:
<ControlTemplate.Resources> <src:RedLetterDayConverter x:Key="conv" /> </ControlTemplate.Resources>
The outer Grid in the CalendarDayButton template then references that binding converter to display a ToolTip:
<Grid ToolTip="{Binding Converter={StaticResource conv}, Mode=OneWay}">
If that binding looks like it's missing a Path setting, keep in mind that the DataContext of the button is a DateTime object, and that DataContext is inherited through the visual tree. If you just wanted to display the DateTime in the ToolTip, you would use simply:
<Grid ToolTip="{Binding}">
The template also has an additional Rectangle object among the others used for highlighting the current day and selected days:
<Rectangle x:Name="RedLetterDayBackground" IsHitTestVisible="False" Fill="#80FF0000" />
A DataTrigger makes this Rectangle invisible when the binding converter returns null:
<DataTrigger Binding="{Binding Converter={StaticResource conv}}" Value="{x:Null}"> <Setter TargetName="RedLetterDayBackground" Property="Visibility" Value="Hidden" /> </DataTrigger>
Figure 8 shows the three holidays for the month of March.
Figure 8 The RedLetterDays Display
In the more general case, the CalendarDayButton template visual tree can include any FrameworkElement derivative that has a dependency property of type DateTime, or even any FrameworkElement derivative that assumes its DataContext is of type DateTime.
A few readers of this column may recall the calendar I created for the January 2008 issue with the phases of the moon. The original MoonDisk class derived from Viewport3D to simulate a view of the moon illuminated by the sun. I eliminated the DateTime dependency property from that class and modified it slightly to cast its DataContext to a DateTime. The MoonPhaseCalendar project includes that MoonDisk class and references it in the visual tree of the CalendarDayButton template like so:
<Grid Width="48" Height="48"> <ContentPresenter x:Name="NormalText" HorizontalAlignment="Left" VerticalAlignment="Top"> <TextElement.Foreground> <SolidColorBrush x:Name="selectedText" Color="#FF333333"/> </TextElement.Foreground> </ContentPresenter> <src:MoonDisk Margin="6" /> </Grid>
Notice there's no binding on MoonDisk because it inherits the DataContext of type DateTime. The result for the month of July 2009 is shown in Figure 9.
Figure 9 The MoonPhaseCalendar Display
Deriving from Calendar
CalendarItem, CalendarButton, and CalendarDayButton are all sealed classes—you can't derive from them, and even if you could derive from CalendarButton and CalendarDayButton, you'd need to rewrite parts of CalendarItem to instantiate your new buttons rather than the old ones.
You can, however, derive from Calendar itself to provide some additional functionality. For example, you might want to enhance the Calendar control so that when you click on a particular day, a modeless dialog pops up, as shown in Figure 10, to let you enter and view a schedule for the day. This is the idea behind the DailyReminders project.
Figure 10 The DailyReminders Dialog Box
To implement this feature, the class that derives from Calendar (which I called DailyRemindersCalendar) must be able to detect when the user is clicking a CalendarDayButton. Normally this would be very straightforward: The constructor of the class that derives from Calendar would call AddHandler with arguments indicating the Mouse.ClickEvent and a method to handle these events. As you know, WPF implements routed events, so the Click event from any child button travels up the visual tree. The handler would discriminate among the various child buttons by checking the Source argument of the RoutedEvent object, and also by checking whether the DataContext is an object of type DateTime.
However, this proved not to be possible. The CalendarItem class installs handlers for the CalendarDayButton button-down and button-up events and disables the Click event.
Instead, I wrote DailyRemindersCalendar to override the OnMouseDoubleClick method. However, that raised another problem: for that event in a Calendar derivative, neither the Source nor OriginalSource properties of MouseButtonEventArgs is any type of Button. Source is null and OriginalSource is either a TextBlock or a Path or Rectangle.
Once again, however, the inheritance of the DataContext through the visual tree comes to the rescue. In the OnMouseDoubleClick override, if the OriginalSource object has a DataContext of type DateTime, the method knows that a CalendarDayButton is being clicked, and the DateTime object tells the method exactly which one. The method can then instantiate a DailyRemindersDialog, which builds its user interface (a Grid containing TextBlock and TextBox controls) entirely in code. (In fact, the entire DailyReminders program consists entirely of code with no markup.)
The DailyRemindersDialog is invoked modelessly, so you can simultaneously display dialogs for different days. However, the DailyRemindersDialog prohibits displaying multiple dialogs for the same day. That would raise issues involved with properly storing and retrieving these daily reminders. The DailyRemindersStorage class handles that part of the program by referencing an XML file in isolated storage. Whenever a DailyRemindersDialog closes, it causes that file to be saved with the updated information.
The DatePicker Control
I have focused exclusively on the new Calendar control rather than the DatePicker because DatePicker consists largely of a DatePickerTextBox and a dropdown that invokes the Calendar control.
This DatePickerTextBox is extremely crude. It certainly does not implement the feature I like so much in the DateTimePicker control in Windows Forms—clicking the individual components of the date or time (year, month, day, hour, and so forth) and using the cursor keys or numeric keys to change the values.
But all the styles and templates you can apply to the standalone Calendar control can also be applied to the Calendar control invoked from the dropdown in DatePicker. The DatePicker control has a property named CalendarStyle of type Style, and the Style object you set to this property can contain setters for any property defined by Calendar, including the CalendarItemStyle, CalendarButtonStyle, and CalendarDayButtonStyle properties.
Though a series of embedded styles and templates, you can in effect reach into both the Calendar control and DatePicker control and tailor them to your own needs and preferences.
Send your questions and comments to mmnet30@microsoft.com.
Charles Petzold is a longtime Contributing Editor to MSDN Magazine. His most recent book is The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine (Wiley, 2008). His Web site is www.charlespetzold.com.