Article Series
This article is part one of a series of articles on developing an advanced WPF control designed to function like the Calendar view in Microsoft Outlook.
Introduction
The previous article covered the design of one of the key pieces to layout appointments, the RangePanel
, which could size and arrange child controls proportionally based upon the attached properties at the control level and standard dependency properties at the panel level. The next part will add in some additional minor definitions around the CalendarView
control template and the layout of individual periods.
Step 6 - Additional CalendarView Control Template Pieces
To better visualize where some of the future pieces will go, the next step is to make some additions to the CalendarView
control template by changing its default style. The control template is divided into three pieces: the header, the time scale, and the content.
<Style x:Key="{ComponentResourceKey
TypeInTargetAssembly={x:Type controls:CalendarView},
ResourceId=DefaultStyleKey}"
TargetType="{x:Type ListView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListView}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Row="0" Grid.Column="1"
BorderBrush="Black"
BorderThickness="1,1,1,1">
<TextBlock x:Name="PART_Header"
Text="Header"
HorizontalAlignment="Center" />
</Border>
<Border Grid.Row="1" Grid.Column="0"
BorderBrush="Black"
BorderThickness="1,1,1,1">
<TextBlock x:Name="PART_TimeScale"
Text="Time Scale"
VerticalAlignment="Center">
<TextBlock.LayoutTransform>
<RotateTransform Angle="270" />
</TextBlock.LayoutTransform>
</TextBlock>
</Border>
<TextBlock x:Name="PART_ContentPresenter"
Grid.Row="1" Grid.Column="1"
Text="Content" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Notice the pieces of the template that have names defined following the form "PART_[Name]". This is a general naming standard for WPF template component naming. The benefit in naming pieces of the template is the ability to then reference those pieces within code, if necessary, via the FrameworkTemplate.FindName
method. This is especially useful when a control might have its control template replaced. For example, assume a basic login control that needs a user ID, password, and a button. If code exists that assumes those controls exist, then the control can be restyled by someone else, just as long as they implement the basic contract. But, how would someone know what the contract is, other than via documentation? The solution is to provide metadata on the class itself in the form of TemplatePartAttribute
attributes. Similar attributes exist on classes like ListBox
, ComboBox
, etc., to provide contract information. The attributes for CalendarView
are in the code shown below:
[TemplatePart(Name="PART_Header", Type=typeof(TextBlock))]
[TemplatePart(Name="PART_TimeScale", Type=typeof(TextBlock))]
[TemplatePart(Name="PART_ContentPresenter", Type=typeof(TextBlock))]
public class CalendarView : ViewBase
{
}
Step 7 - CalendarViewContentPresenter/CalendarViewPeriodPresenter Components
Two pieces will be started in this step. The first is the CalendarViewContentPresenter
, which will replace the "Content" TextBox
in the template. The purpose of it will be to contain one CalendarViewPeriodPresenter
for each period within the CalendarView
. This presents a challenge. The content presenter will be the logical parent of the ListViewItem
controls, but visually, it will be the parent of the period presenter controls. This will require overriding the visual tree of the control via code. This core of this procedure is to override the VisualChildCount
property and the GetVisualChild
method (both from the Visual
class).
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using OutlookWpfCalendar.Windows.Controls;
namespace OutlookWpfCalendar.Windows.Primitives
{
public class CalendarViewContentPresenter : Panel
{
private UIElementCollection visualChildren;
private bool visualChildrenGenerated;
protected CalendarView CalendarView
{
get { return this.ListView.View as CalendarView; }
}
protected ListView ListView
{
get { return this.TemplatedParent as ListView; }
}
protected override Size ArrangeOverride(Size finalSize)
{
int columnCount = this.CalendarView.Periods.Count;
Size columnSize = new Size(finalSize.Width / columnCount,
finalSize.Height);
double elementX = 0;
foreach (UIElement element in this.visualChildren)
{
element.Arrange(new Rect(new Point(elementX, 0), columnSize));
elementX = elementX + columnSize.Width;
}
return finalSize;
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
return constraint;
}
protected override int VisualChildrenCount
{
get
{
if (this.visualChildren == null)
return base.VisualChildrenCount;
return this.visualChildren.Count;
}
}
protected override Visual GetVisualChild(int index)
{
if ((index < 0) || (index >= this.VisualChildrenCount))
throw new ArgumentOutOfRangeException("index",
index, "Index out of range");
if (this.visualChildren == null)
return base.GetVisualChild(index);
return this.visualChildren[index];
}
protected void GenerateVisualChildren()
{
if (this.visualChildrenGenerated)
return;
if (this.visualChildren == null)
visualChildren = this.CreateUIElementCollection(null);
else
visualChildren.Clear();
foreach (CalendarViewPeriod period in CalendarView.Periods)
this.visualChildren.Add(new CalendarViewPeriodPresenter()
{ Period = period });
this.visualChildrenGenerated = true;
}
}
}
To override the visual tree, a separate UIElementCollection
is created and populated via the GenerateVisualChildren
method. The Visual
class property and method described above are redirected to this new collection (using the default implementation in the event the override collection does not exist). Generating the visual collection creates a CalendarViewPeriodPresenter
assigned to the period in question. The generation of the visual children is tied to the MeasureOverride
method, so that it is triggered upon rendering of the control. However, since this method is called on events such as a resize, a flag is used to avoid excess re-creations of the visual children. One addition for later might be a listener on the Periods
to force re-generation when the Periods
collection changes. Within the ArrangeOverride
, each of the presenter controls is laid out into equal size columns filling the available real estate of the control.
using System.Windows;
using System.Windows.Controls;
using OutlookWpfCalendar.Windows.Controls;
namespace OutlookWpfCalendar.Windows.Primitives
{
public class CalendarViewPeriodPresenter: Control
{
static CalendarViewPeriodPresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarViewPeriodPresenter),
new FrameworkPropertyMetadata(typeof(CalendarViewPeriodPresenter)));
}
public CalendarViewPeriod Period { get; set; }
}
}
The XAML:
<Style TargetType="{x:Type primitives:CalendarViewPeriodPresenter}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type primitives:CalendarViewPeriodPresenter}">
<Border BorderBrush="Black" BorderThickness="1,1,1,1">
<TextBlock Text="{Binding RelativeSource={RelativeSource
TemplatedParent}, Path=Period.Header}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The CalendarViewPeriodPresenter
control, at this point, is just an empty control with a Period
property. A new piece is the static constructor, which overrides the default style of the control. Without this line, the style put into Generic.xaml would not take effect, as the period presenter would use the default style of its parent, Control
. For the moment, the control will just display a border to outline the space taken for each period, along with a TextBlock
identifying the period represented by the control. With the use of calendar periods now in the control in some form, the CalendarViewWindow
can be modified to contain the CalendarView
defined below, along with a screenshot showing the results of the changes.
<controls:CalendarView ItemBeginBinding="{Binding Path=Start}"
ItemEndBinding="{Binding Path=Finish}">
<controls:CalendarView.Periods>
<controls:CalendarViewPeriod Begin="03/02/2009 12:00 AM"
End="03/02/2009 8:00 AM" Header="Monday" />
<controls:CalendarViewPeriod Begin="03/03/2009 12:00 AM"
End="03/03/2009 8:00 AM" Header="Tuesday" />
<controls:CalendarViewPeriod Begin="03/04/2009 12:00 AM"
End="03/04/2009 8:00 AM" Header="Wednesday" />
<controls:CalendarViewPeriod Begin="03/05/2009 12:00 AM"
End="03/05/2009 8:00 AM" Header="Thursday" />
<controls:CalendarViewPeriod Begin="03/06/2009 12:00 AM"
End="03/06/2009 8:00 AM" Header="Friday" />
</controls:CalendarView.Periods>
</controls:CalendarView>
Step 8 - Generate and Place ListViewItem Controls (Ignore Appointment Overlap)
The next step in the process is to generate and place the ListViewItem
s in their appropriate spots. For the moment, we'll ignore the issue of appointment overlap. This step will be fairly lengthy, and will require a number of changes.
First, we'll modify the CalendarViewContentPresenter
to generate the ListViewItem
controls. Normally, this step is not necessary in a custom control, since they are already being generated (set a breakpoint in a method such as ArrangeOverride
and examine the base.Children
property to see them). However, these Visual
controls already have their visual parent set to be the CalendarViewContentPresenter
itself, which is not what is needed. The reason this code is done here (vs. generating them within each period presenter, for example) is that this step needs to be done within the control set to be the ItemsHost
.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using OutlookWpfCalendar.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Collections.Generic;
namespace OutlookWpfCalendar.Windows.Primitives
{
public class CalendarViewContentPresenter : Panel
{
private List<UIElement> listViewItemVisuals;
private bool listViewItemVisualsGenerated;
internal List<UIElement> ListViewItemVisuals
{
get { return listViewItemVisuals; }
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
this.GenerateListViewItemVisuals();
return constraint;
}
protected void GenerateListViewItemVisuals()
{
if (this.listViewItemVisualsGenerated)
return;
IItemContainerGenerator generator =
((IItemContainerGenerator)
ListView.ItemContainerGenerator).GetItemContainerGeneratorForPanel(this);
generator.RemoveAll();
if (this.listViewItemVisuals == null)
this.listViewItemVisuals = new List<UIElement>();
else
this.listViewItemVisuals.Clear();
using (generator.StartAt(new GeneratorPosition(-1, 0),
GeneratorDirection.Forward))
{
UIElement element;
while ((element = generator.GenerateNext() as UIElement) != null)
{
this.listViewItemVisuals.Add(element);
generator.PrepareItemContainer(element);
}
this.listViewItemVisualsGenerated = true;
}
}
}
}
Once generated, each ListViewItem
will need the RangePanel
Begin
/End
attached properties set on it. This will be done by creating DateTime
attached properties that will link to each ListViewItem
. With this, it will be possible to ask each ListViewItem
what the appointment beginning and end are. How does this get evaluated? By using the binding provided to the CalendarView
instance (ItemBeginBinding
, ItemEndBinding
).
public class CalendarView : ViewBase
{
public static DependencyProperty BeginProperty =
DependencyProperty.RegisterAttached("Begin",
typeof(DateTime), typeof(ListViewItem));
public static DependencyProperty EndProperty =
DependencyProperty.RegisterAttached("End",
typeof(DateTime), typeof(ListViewItem));
public static DateTime GetBegin(DependencyObject item)
{
return (DateTime)item.GetValue(BeginProperty);
}
public static DateTime GetEnd(DependencyObject item)
{
return (DateTime)item.GetValue(EndProperty);
}
public static void SetBegin(DependencyObject item, DateTime value)
{
item.SetValue(BeginProperty, value);
}
public static void SetEnd(DependencyObject item, DateTime value)
{
item.SetValue(EndProperty, value);
}
protected override void PrepareItem(ListViewItem item)
{
item.SetBinding(BeginProperty, ItemBeginBinding);
item.SetBinding(EndProperty, ItemEndBinding);
}
}
With the content presenter and CalendarView
changes made, the CalendarViewPeriodPresenter
can be modified. This is a replacement for the existing placeholder (which was just a control template containing a TextBlock
). Part of the modifications include removing the existing default style from Generic.xaml as the visual tree will be created via code. Much of the code will seem familiar as the same "override visual tree via code" concept used in the content presenter. There are two primary changes. First, the only visual child at this point is a single RangePanel
. For the moment, appointment overlap is ignored, but since a future change will possibly require more than one RangePanel
for each period, a collection will be used instead of a single instance. The second addition is the initialization of the RangePanel
itself. The RangePanel
's Minimum
and Maximum
are bound to the Begin
and End
of the Period
instance provided (using the Ticks
property to provide a double
value). Also, each ListViewItem
is checked to see if it belongs in the Period
, and if so, it is added to the RangePanel
. The RangePanel
attached properties defining the Begin
and End
of the item are set based upon the CalendarView
attached properties created above.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using OutlookWpfCalendar.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Collections.Generic;
namespace OutlookWpfCalendar.Windows.Primitives
{
public class CalendarViewPeriodPresenter: Panel
{
private bool visualChildrenGenerated;
private UIElementCollection visualChildren;
static CalendarViewPeriodPresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(CalendarViewPeriodPresenter),
new FrameworkPropertyMetadata(typeof(CalendarViewPeriodPresenter)));
}
public CalendarViewPeriod Period { get; set; }
public ListView ListView { get; set; }
public CalendarView CalendarView { get; set; }
private CalendarViewContentPresenter ContentPresenter
{
get
{
return (CalendarViewContentPresenter)this.Parent;
}
}
protected override int VisualChildrenCount
{
get
{
if (this.visualChildren == null)
return base.VisualChildrenCount;
return this.visualChildren.Count;
}
}
protected override Visual GetVisualChild(int index)
{
if ((index < 0) || (index >= this.VisualChildrenCount))
throw new ArgumentOutOfRangeException("index",
index, "Index out of range");
if (this.visualChildren == null)
return base.GetVisualChild(index);
return this.visualChildren[index];
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement element in this.visualChildren)
element.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
return constraint;
}
protected void GenerateVisualChildren()
{
if (visualChildrenGenerated)
return;
if (this.visualChildren == null)
this.visualChildren = this.CreateUIElementCollection(null);
else
this.visualChildren.Clear();
RangePanel panel = new RangePanel();
panel.SetBinding(RangePanel.MinimumProperty,
new Binding("Begin.Ticks") { Source = Period });
panel.SetBinding(RangePanel.MaximumProperty,
new Binding("End.Ticks") { Source = Period });
foreach (ListViewItem item in this.ContentPresenter.ListViewItemVisuals)
{
if (this.CalendarView.PeriodContainsItem(item, this.Period))
{
item.SetValue(RangePanel.BeginProperty,
Convert.ToDouble(((DateTime)item.GetValue(
CalendarView.BeginProperty)).Ticks));
item.SetValue(RangePanel.EndProperty,
Convert.ToDouble(((DateTime)item.GetValue(
CalendarView.EndProperty)).Ticks));
panel.Children.Add(item);
}
}
Border border = new Border() { BorderBrush = Brushes.Blue,
BorderThickness = new Thickness(1.0) };
border.Child = panel;
visualChildren.Add(border);
this.visualChildrenGenerated = true;
}
}
}
public class CalendarView : ViewBase
{
public bool PeriodContainsItem(ListViewItem item, CalendarViewPeriod period)
{
DateTime itemBegin = (DateTime)item.GetValue(BeginProperty);
DateTime itemEnd = (DateTime)item.GetValue(EndProperty);
return (((itemBegin <= period.Begin) &&
(itemEnd >= period.Begin)) ||
((itemBegin <= period.End) &&
(itemEnd >= period.Begin)));
}
}
The changes made will produce the result shown below, which shows the control now starting to take shape.
Next Steps
A number of advanced WPF topics have been covered in the previous two sections. The next steps of the control development are going to be more straightforward; more visual and less functional as the header and time scale portions of the control get filled out.
History
- 04/21/2009: Initial version.