Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating the Microsoft Outlook Appointment View in WPF - Part 3

0.00/5 (No votes)
21 Apr 2009 1  
Part 3 of a series describing the creation of a WPF based appointment control with the Outlook look and feel.

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>

OutlookWpfCalendarPart3_1.png

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>

OutlookWpfCalendarPart3_2.png

Step 8 - Generate and Place ListViewItem Controls (Ignore Appointment Overlap)

The next step in the process is to generate and place the ListViewItems 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
    {
        // Pre-existing code removed to isolate changes
        
        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));

    // Pre-existing code removed to isolate changes
    
    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
{
    // Pre-existing code removed to isolate changes

    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.

OutlookWpfCalendarPart3_3.png

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here