Styling the WPF Calendar to Resemble Outlook's Month View Calendar

This article shows how to style the WPF calendar to resemble Microsoft Outlook Calendar in Month view and how to add appointments.

Introduction

I was in search for a calendar where appointments can be added. I've seen a great CodeProject article by kirkaiya where he achieved this by creating a UserControl. It's very good but I thought I could achieve almost the same thing by styling the WPF Calendar. Figure 1 shows the WPF calendar.


Figure 1. WPF Calendar in its Default Style

My first impression is that it might not be possible. Anyway, I wanted to give it shot. I came across another article at MSDN Magazine, this time by Charles Petzold. He explained in great detail the different parts of the Calendar's template. After reading the article, it seems to me that it is not enough to change the Calendar's template. A custom control that inherits from Calendar should be created to support appointments.

Getting Started

The best way to style a WPF control is to start from the default style. Fortunately, we don't need any tools to get the default style. The source code is freely available at CodePlex, so I recommend downloading it first. Upon extracting the zip file, you can get the Calendar's default style from WPFToolkitBinariesAndSource\Toolkit-release\Calendar\Themes\Generic.xaml. This file has a resource dictionary containing 4 styles for the following controls: Calendar, CalendarItem, CalendarDayButton and CalendarButton.

The Calendar's control template only consists of a CalendarItem control inside a StackPanel. This means that the default appearance of the Calendar is mostly defined by the CalendarItem's control template. However, you can still add other controls to the Calendar's template.

The CalendarItem control is basically divided into the following parts: previous button, next button, header button, month content grid, and year content grid. The appearance of the CalendarItem depends on the selected display mode: month, year or decade.

The month content grid is used when the display mode is month while the year content grid is used for the other two display modes. The CalendarItem fills up the month content grid with CalendarDayButton controls. Meanwhile, the year content grid is filled up with CalendarButton controls. Since we are only interested in the month view of the calendar, we don't need to edit the CalendarButton template.

Let's create a WPF application that shows a calendar in the main window. Copy the default styles for Calendar, CalendarItem and CalendarDayButton controls in the window's resources and add the required assemblies and namespaces. If you are using Visual Studio 2008 and .NET 3.5 SP1, you might get the following error: "The attachable property 'VisualStateGroups' was not found in type 'VisualStateManager'. This is a known bug and you won't be able to see the Calendar in the WPF designer. However, the application should be able to run without any errors.

Resizing the Calendar

The Calendar should fill the entire grid and resized when the main window is resized. This can't be done just by setting the width and height of the Calendar. First, let's change the Calendar's control template. I changed the StackPanel into a Grid and removed the HorizontalAlignment.

<Grid Name="PART_Root">
<primitives:CalendarItem
Name="PART_CalendarItem"
Style="{TemplateBinding CalendarItemStyle}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
/>
</Grid>

Listing 1. Calendar Control Template

Although the CalendarItem fills the grid, the CalendarItem's content (the navigation buttons, header button and month content grid) is aligned to the top left corner of the grid. So we need to edit the CalendarItem's template so that the content is distributed evenly. But first, we should give a key to the CalendarItem style and use it as the value of the Calendar's CalendarItemStyle property. After that, look for the grid that has 2 rows and 3 columns in the CalendarItem's control template. The first row contains the navigation and header buttons. The second row contains the month content grid. The first column contains the previous button, second column contains the header button and third column contains the next column. The month content grid spans 3 columns. Let's add another column that will occupy the remaining space. The month content grid will now span 4 columns.

We should also change the row and column definitions of the month content grid. This grid contains 7 rows and 7 columns, where the Height and Width properties are set to Auto. The first row corresponds to the day title (Su, Mo, Tu, etc.) while the other 6 rows contain the days of the month (1, 2, 3 and so on). The 7 columns correspond to the 7 days of the week. Let's set the Height of the last 6 rows and the Width of all columns to *. These changes result in the following figure.


Figure 2. Resized Calendar

Styling the Navigation and Header Buttons

The navigation buttons in Outlook are displayed as arrows inside circles. The displayed month is also not in the middle of the navigation buttons, but located at the right. The font properties are also a bit different. Let's start first with the circle background of the navigation buttons.

<Style x:Key="NavigationEllipseStyle" TargetType="{x:Type Ellipse}">
<Setter Property="Width" Value="20"/>
<Setter Property="Height" Value="20"/>
<Setter Property="Stroke" Value="#FF8EB0DC"/>
<Setter Property="StrokeThickness" Value="1"/>
<Setter Property="Fill">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#FFFAFCFF" Offset="0"/>
<GradientStop Color="#FFFAFCFF" Offset="0.5"/>
<GradientStop Color="#FFCCE2FF" Offset="0.5"/>
<GradientStop Color="#FFCCE2FF" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>

Listing 2. The Navigation Button's Ellipse

I added this style in the resources section of the grid containing the navigation buttons. The following code shows the inner grid of the updated previous button template which uses the style shown in Listing 2. The path objects that make up the arrow and other properties like margin and width are also updated.

<Grid>
<Ellipse Style="{StaticResource NavigationEllipseStyle}"/>
<Path Margin="4,0,0,0" Height="10" Width="6" VerticalAlignment="Center" HorizontalAlignment="Left" Stretch="Fill" Data="M288.75,232.25 L283,236.625 L288.75,240.625" StrokeThickness="2">
<Path.Stroke>
<SolidColorBrush x:Name="TextColor" Color="#FF406CA6" />
</Path.Stroke>
</Path>
<Path Margin="4,0,0,0" Height="10" Width="12" VerticalAlignment="Center" HorizontalAlignment="Left" Stretch="Fill" Data="M283,236.625 L297,236.625" Stroke="#FF406CA6" StrokeThickness="2"/>
</Grid>

Listing 3. Part of Updated Previous Button Template

In the following code, I added a margin around the previous button and set its width to 20 instead. I just applied the same thing with the next button, with the arrow's paths reversed.

<Button x:Name="PART_PreviousButton"
Margin="4"
Grid.Row="0" Grid.Column="0"
Template="{StaticResource PreviousButtonTemplate}"
Height="20" Width="20"
HorizontalAlignment="Left"
Focusable="False"
/>

Listing 4. Updated Previous Button

Finally, I changed the font weight and size properties of the header button. Also notice that the header and next buttons have switched places.

<Button x:Name="PART_HeaderButton"
Grid.Row="0" Grid.Column="2"
Template="{StaticResource HeaderButtonTemplate}"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="Normal" FontSize="20"
Focusable="False"
/>

Listing 5. Updated Header Button

The following figure shows the updated Calendar.


Figure 3. Updated Navigation and Header Buttons

Changing the Day Title Format

As you see, the format of each day title is set to the shortest day name like Su for Sunday, Mo for Monday, and so on. Unfortunately, the Calendar or the CalendarItem does not expose any property that let's us change the day title format easily. The CalendarItem uses only the ShortestDayNames property of the current date format. However, we can still change the format by using an IValueConverter as you will see later. The following code shows the updated DayTitleTemplate.

<local:DayNameConverter x:Key="DayNameConverter"/>

<!-- Start: Data template for header button -->
<DataTemplate x:Key="DayTitleTemplate">
<Grid>
<TextBlock
FontWeight="Normal"
FontFamily="Arial"
FontSize="10.5"
Foreground="#FF7C93D7"
HorizontalAlignment="Center"
Text="{Binding Converter={StaticResource DayNameConverter}}"
Margin="0,6,0,6"
VerticalAlignment="Center"/>
</Grid>
</DataTemplate>

Listing 6. Updated Day Title Template

The Text property of the TextBlock is bound to a string representing the shortest day name. So if we want to change Su to Sunday, Mo to Monday and so on, we need to use an IValueConverter, like the one shown below.

/// <summary>
/// Converts the specified short day name to its normal counterpart.
/// </summary>
[ValueConversion(typeof(string), typeof(string))]
public class DayNameConverter : IValueConverter
{
object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
DateTimeFormatInfo dateTimeFormat = GetCurrentDateFormat();
string[] shortestDayNames = dateTimeFormat.ShortestDayNames;
string[] dayNames = dateTimeFormat.DayNames;

for
(int i = 0; i < shortestDayNames.Count(); i++)
{
if (shortestDayNames[i] == value.ToString())
{
return dayNames[i];
}
}

return
null;
}

object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

private
static DateTimeFormatInfo GetCurrentDateFormat()
{
if (CultureInfo.CurrentCulture.Calendar is GregorianCalendar)
{
return CultureInfo.CurrentCulture.DateTimeFormat;
}

foreach (Calendar cal in CultureInfo.CurrentCulture.OptionalCalendars)
{
if (cal is GregorianCalendar)
{
DateTimeFormatInfo dtfi = new CultureInfo(CultureInfo.CurrentCulture.Name).DateTimeFormat;
dtfi.Calendar = cal;
return dtfi;
}
}

DateTimeFormatInfo
dt = new CultureInfo(CultureInfo.InvariantCulture.Name).DateTimeFormat;
dt.Calendar = new GregorianCalendar();

return
dt;
}
}

Listing 7. Day Name Converter

This IValueConverter converts the shortest day name to its normal day name equivalent. The Convert method uses the GetCurrentDateFormat method in getting the current date format. This is the same method used by the CalendarItem in generating the day titles. The following figure shows the updated Calendar. Notice that I also changed the text color.


Figure 4. Updated Day Titles

Styling the CalendarDayButton

In Outlook, the days are located on the top left corner of each cell. Also, there is a filled rectangle on top of each cell. To imitate this in our Calendar, we'll need to update the CalendarDayButton template. The following XAML code is a part of the updated CalendarDayButton control template.

<Rectangle x:Name="SelectedBackground" Grid.Row="1" RadiusX="1" RadiusY="1" Opacity="0" Fill="{TemplateBinding Background}"/>
<Rectangle x:Name="Background" Grid.Row="1" RadiusX="1" RadiusY="1" Opacity="0" Fill="{TemplateBinding Background}" />
<Rectangle x:Name="InactiveBackground" Grid.Row="1" RadiusX="1" RadiusY="1" Opacity="0" Fill="#FFA5BFE1"/>
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop x:Name="StartGradient" Color="#FFDFE8F5" Offset="0"/>
<GradientStop Color="{Binding ElementName=StartGradient, Path=Color}" Offset="0.5"/>
<GradientStop x:Name="EndGradient" Color="#FFC9D9ED" Offset="0.5"/>
<GradientStop Color="{Binding ElementName=EndGradient, Path=Color}" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<ContentPresenter
x:Name="NormalText"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="5,1,5,1">
<TextElement.Foreground>
<SolidColorBrush x:Name="selectedText" Color="#FF333333"/>
</TextElement.Foreground>
</ContentPresenter>
</Border>
<Rectangle x:Name="Border" StrokeThickness="0.5" Grid.RowSpan="2" SnapsToDevicePixels="True">
<Rectangle.Stroke>
<SolidColorBrush x:Name="BorderBrush" Color="#FF5D8CC9"/>
</Rectangle.Stroke>
</Rectangle>
<Path x:Name="Blackout" Grid.Row="1" Opacity="0" Margin="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RenderTransformOrigin="0.5,0.5" Fill="#FF000000" Stretch="Fill" Data="M8.1772461,11.029181 L10.433105,11.029181 L11.700684,12.801641 L12.973633,11.029181 L15.191895,11.029181 L12.844727,13.999395 L15.21875,17.060919 L12.962891,17.060919 L11.673828,15.256231 L10.352539,17.060919 L8.1396484,17.060919 L10.519043,14.042364 z"/>
<Rectangle Width="0" x:Name="DayButtonFocusVisual" Grid.Row="1" Visibility="Collapsed" IsHitTestVisible="false" RadiusX="1" RadiusY="1" Stroke="#FF45D6FA"/>

Listing 8. Part of Updated CalendarDayButton Template

If you look at the default style, there is a Rectangle labeled TodayBackground. You can see this as the gray background of the grid containing the current day. I've removed this since Outlook indicates the current day differently. I've also added another row so that the first row contains the day and has a blue background (this is the LinearGradientBrush in the above XAML) while the second row has a white background and will show the appointments for that day. More on that later.

When the current VisualState is set to Today, the following animations take place. Basically, it changes the background color of the rectangle containing the day and the border's color and stroke thickness.

<vsm:VisualState x:Name="Today">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="StartGradient" Storyboard.TargetProperty="Color" To="#FFF7D275"></ColorAnimation>
<ColorAnimation Duration="0" Storyboard.TargetName="EndGradient" Storyboard.TargetProperty="Color" To="#FFF3B84B"></ColorAnimation>
<ColorAnimation Duration="0" Storyboard.TargetName="BorderBrush" Storyboard.TargetProperty="Color" To="#FFF3B84B"></ColorAnimation>
<DoubleAnimation Duration="0" Storyboard.TargetName="Border" Storyboard.TargetProperty="StrokeThickness" To="2"></DoubleAnimation>
</Storyboard>
</vsm:VisualState>

Listing 9. StoryBoard when VisualState is Today

I've also added a rectangle and named it InactiveBackground. Its opacity is set to 1 when the VisualState is Inactive. You can see this background for days that are shown on the calendar but is not included in the currently displayed month. For example, Oct. 31 is shown but is inactive when the current month displayed is November.

<vsm:VisualState x:Name="Inactive">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="selectedText" Storyboard.TargetProperty="Color" To="#FF777777"></ColorAnimation>
<DoubleAnimation Duration="0" Storyboard.TargetName="InactiveBackground" Storyboard.TargetProperty="Opacity" To="1"></DoubleAnimation>
</Storyboard>
</vsm:VisualState>

Listing 10. StoryBoard when VisualState is Inactive

The following figure shows the updated Calendar.



Figure 5. Styled CalendarDayButton

Finalizing the Style

There are some minor things that still need to be done, like setting the margins, borders, background etc. The most noticeable is the light-blue background at the top of the Calendar. The background's height resizes proportionally with the height of the window. This is not the behavior we want. We can remove this by not specifying the background of the CalendarItem in the Calendar’s control template. The following figure shows the final Calendar after applying or updating some margins and adding background to some grid rows.


Figure 6. Final Calendar Style

Adding Appointments

To add appointments, we need to create a custom control that will inherit from the Calendar control. This will mean that we should move the style definitions from the window’s resources section into the custom control’s resource dictionary. This custom control, shown in the code below, will have a variable that will contain the list of appointments.

/// <summary>
/// Custom calendar control that supports appointments.
/// </summary>
public class MonthViewCalendar : Calendar
{
public static DependencyProperty AppointmentsProperty =
DependencyProperty.Register
(
"Appointments",
typeof(ObservableCollection<Appointment>),
typeof(Calendar)
);

///
<summary>
/// The list of appointments. This is a dependency property.
/// </summary>
public ObservableCollection<Appointment> Appointments
{
get { return (ObservableCollection<Appointment>)GetValue(AppointmentsProperty); } set { SetValue(AppointmentsProperty, value); }
}

static
MonthViewCalendar()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MonthViewCalendar), new FrameworkPropertyMetadata(typeof(MonthViewCalendar)));
}

public
MonthViewCalendar()
: base()
{
SetValue(AppointmentsProperty, new ObservableCollection<Appointment>());
}
}

Listing 11. Custom Calendar Control

The Appointments collection accepts objects of type Appointment, which is shown below.

/// <summary>
/// The appointment data.
/// </summary>
public class Appointment
{
public string Subject { get; set; }

public
DateTime Date { get; set; }
}

Listing 12. Appointment Class

This class contains only a few details. I won’t imitate Outlook’s implementation of adding appointments. In this implementation, when a user double-clicks on a CalendarDayButton, the appointment dialog will be shown. The following figure shows the appointment dialog.


Figure 7. Appointment Dialog

My first idea was to derive from CalendarDayButton. But it impossible as the CalendarDayButton is a sealed class. After reading Charles Petzold’s article I mentioned in the Introduction, my only choice is to override the OnMouseDoubleClick method of the Calendar class. The following shows the overridden method.

protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
{
base.OnMouseDoubleClick(e);

FrameworkElement
element = e.OriginalSource as FrameworkElement;

if
(element.DataContext is DateTime)
{
AppointmentWindow appointmentWindow = new AppointmentWindow
(
(Appointment appointment) =>
{
Appointments.Add(appointment);
}
);

appointmentWindow.Show();
}
}

Listing 13. The OnMouseDoubleClick Method

The tricky part is how we will know if the user really clicked a CalendarDayButton. The OriginalSource property of the MouseButtonEventArgs object will not return an object of type CalendarDayButton. It might return any object in the CalendarDayButton’s control template, like rectangle or path. Fortunately, the DataContext value of the CalendarDayButton is inherited by controls deeper in the visual tree. Thus, if the DataContext property is of type DateTime, we know that the user clicked on a CalendarDayButton.

The next thing to do is display the appointments. This can be achieved by updating the CalendarDayButton control template. The basic idea is to add a ListBox where its ItemsSource property is bound to the Appointments property of the MonthViewCalendar control. We also need to filter the appointments based on the date of the CalendarDayButton. I used an IMultiValueConverter for this purpose. The following XAML code is a part of the updated CalendarDayButton template.

<!-- Appointments -->

<ListBox
x:Name="appointmentsLbx"
Grid.Row="1"
Background="Transparent"
BorderBrush="Transparent"
HorizontalContentAlignment="Stretch"
>
<ListBox.ItemsSource>
<MultiBinding Converter="{StaticResource AppointmentsConverter}">
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MonthViewCalendar}}" Path="Appointments"/>
<Binding RelativeSource="{RelativeSource Mode=Self}" Path="DataContext"/>
</MultiBinding>
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="#FFDFE8F5" BorderBrush="#FF5D8CC9" BorderThickness="1" CornerRadius="4">
<TextBlock HorizontalAlignment="Center" Text="{Binding Subject}"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<ControlTemplate.Triggers>
<Trigger SourceName="appointmentsLbx" Property="HasItems" Value="False">
<Setter TargetName="appointmentsLbx" Property="Visibility" Value="Hidden"/>
</Trigger>
</ControlTemplate.Triggers>

Listing 14. CalendarDayButton’s Appointments ListBox

I used multi-binding to filter the appointments based on the date on the CalendarDayButton. This is because we need to pass 2 objects to the IMultiValueConverter: the appointments collection and the date. The following shows the IMultiValueConverter code.

/// <summary>
/// Gets the appointments for the specified date.
/// </summary>
[ValueConversion(typeof(ObservableCollection<Appointment>), typeof(ObservableCollection<Appointment>))]
public class AppointmentsConverter : IMultiValueConverter
{
#region IMultiValueConverter Members

public
object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime date = (DateTime)values[1];

ObservableCollection
<Appointment> appointments = new ObservableCollection<Appointment>();
foreach (Appointment appointment in (ObservableCollection<Appointment>)values[0])
{
if (appointment.Date.Date == date)
{
appointments.Add(appointment);
}
}

return
appointments;
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}

#endregion
}

Listing 15. Appointments Converter

Everything should work now except for 1 minor glitch. The list box’s items do not get updated immediately after adding an appointment to the collection. This can be fixed by implementing the INotifyPropertyChanged interface on the MonthViewCalendar class and raising the PropertyChanged event when an appointment is added. The following figure shows the calendar with some appointments.


Figure 9. Calendar with Appointments

The Visual Studio 2008 solution can be downloaded here. Note that I haven’t implemented updating an appointment.

By Michael Detras   Popularity  (32927 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog