Click here to Skip to main content
Click here to Skip to main content

WPF: A TimeLineControl

, 14 Apr 2010
Rate this:
Please Sign up or sign in to vote.
Simple WPF TimeLineControl That I Think May Be Useful
Prize winner in Competition "Best C# article of April 2010"
Prize winner in Competition "Best overall article of April 2010"

Table Of Contents

Introduction

At work we have an ongoing project which uses scheduled items, and we have to display these items to the user. Now we can get literally thousands of these recurrent items, and guess what display metaphor we went for to make it easy for the user to see. Can you guess, no. Ok we went for a List. Mmmmm.

I was less than happy with this and kept thinking there must be a better way to display data that is across a timespan. Some sort of time line control if you will. So I launched my trusty browser and went into Google and had a look around. I did find an absolutely brilliant control at http://timeline.codeplex.com/ which unfortunately for me is currently only available for Silverlight (WPF version is coming but not yet). And although it was fantastic it was not quite what I was after, so I thought may be I'll give it a try and see what I come up with.

The upshot of this is that I have created what I think is a pretty re-usable TimeLine control for WPF. Note it is for WPF only, and I doubt it will ever work in Silverlight due to a DLL dependency on a 3rd party library it has.

The rest of this article will outline how the control works and what you will need to do to use it in your own project. I shall also talk about how to Re-Style the control, in case heaven forbid you do not get on with my own Styling.

Pre-Requisites

There is really only one pre-requisite for this control, which is VS2008 or VS2010 if you have it.

Demo Video

This article is best demonstrated with a Video (note there is no audio), but I will explain all the video working in detail within this article.

Simply click on this image which will take you to a new page showing the video. But before you do, I urge you to read about the points to look out for in the video before you view the video, as by looking out for these points, you will gain a better understanding of how the code associated with this article works.

These are some of the things that you should take note of whilst watching the video.

  • That the user is able to pan around the decades
  • That the user is able to navigate down/back up the date trail
  • That the user is able to inspect exactly what ITimeLineItems (more on this later) make up a single bar by clicking on the bar
  • That clicking on a single ITimeLineItem either from within the popup from the bar, or by clicking on one of the ITimeLineItem items within the ViewingSpecificDayState.

If you missed all that, which I am sure you will the 1st time, I urge you to review the video as it will help you understand the rest of this article a bit better.

What Does It Do

Put simply, this TimeLineControl allows the user to view a series of ITimeLineItem (more on this later) based items in an easy to navigate manner. There are a series of different visual representations that allow the user to drill down into the data, and also allows the user to navigate backwards. At each stage (except the final visual representation) all ITimeLineItem items that match the given viewing criteria are shown in a simple bar graph (as most people understand them), which when clicked will show a popup with a full list of the bars contained ITimeLineItem items.

The attached TimeLineControl, starts out with showing decades and from there the user can drill into a particular decade, and then the decades visualiser will be replaced by a single view of the user selected decade, and from there the user can drill further into a particular year and so on. At any point, the user may choose to navigate backwards from where they have just come from, it is a sort of breadcrumb of times visited if you like.

One thing of note is that at any stage within the navigation process, the user is able to easily see what exact ITimeLineItem based items make up a particular bar just by clicking on the bar itself. If the user finds what they are after, they can simply click on the popup contained ITimeLineItem item, and an TimeLineControl.TimeLineItemClicked is raised that the user can use in their own code, to maybe navigate to a fuller detail page about the selected ITimeLineItem based item.

How Does It All Work

The next couple of sections will outline the overall internal design and talk you through how it works.

The Basic Idea

The basic idea is obviously an extremely well er basic one, we want to show some data that has timestamps data, say DateTime property associated with it. And we would like to show this timeline data in a manner that makes it easy for the user to navigate to the point where they have found what they are looking for. In a nutshell that is really all we are trying to do, we simply want a control that can display timeline data to the user.

So what should this time line data look like then. Well I had a think about this, and at a minimum, I thought it should contain the following data:

  • Description
  • DateTime

So with that in mind, the attached TimeLineControl expects the user to pass in a ObservableCollection<T> where T is some data class that implements the TimeLineControl.ITimeLineItem interface, where the TimeLineControl.ITimeLineItem interface looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TimeLineControl
{
    /// <summary>
    /// Expected interface that must be implemented
    /// to add items to the <c>TimeLineControl</c>
    /// </summary>
    public interface ITimeLineItem
    {
        String Description { get; set; }
        DateTime ItemTime { get; set; }
    }
}

So once the TimeLineControl has been handed (yes you will need to supply the items, it is not that clever, hell if I could make stuff that clever I would be creating a terminator man) ObservableCollection<Your ITimeLineItem based items> items, it is simply a matter of rendering them. Well I say simply, but to be honest this control contains many moving parts (albeit they are all similar in nature), have a look at the project structure:

See there is quite a lot there for a single control, that essentially draws items in time. Don't be put of by this though. As we proceed with the article, you will see that a lot of this is simliar'ish code and once you get one bit the rest will fall into place.

It's All About The States

As I wanted this control to be able to display quite a lot of data, and allow the user to navigate in/out, it seemed only natural that I allow the users to view all the way from decades to a single day. I had a small think about this and although I could have done most of this logic in one control it just seemed quite wrong to me, so I had a bit more of a think, and thought to myself this is basically the State Pattern, which is typically shown as the following UML diagram.

The idea is that you have some Context object which has a single current state, and it makes requests on its current state object. The current state will run some logic that may or may put the Context object into a new state. It is typical for the Context to start in a known state, in fact it's really a must.

For the attached demo code, the UML diagram looks like this:

Within the attached demo code, the Context object is a UserControl called TimeLineControl, and it holds a current IState (state really) object. The TimeLineControl starts out in the ViewingDecadesState. This state diagram further illustrates the inner mechanisms of how the transition from one state to another works.

It was a bit tricky thinking up how all the states should talk to the Context object, as the states themselves are simple data classes, but the TimeLineControl Context object is a UserControl, and the states also required that sort of visual should be rendered for the state. So what I thought up was the idea of having a UserControl associated with a given state, and that state associated UserControl should be able to communicate with the TimeLineControl Context object. This is discussed in more detail in the next section.

Separation Of Concerns / Composition (Ok ok, so it's not all about the states)

Ok so when I just stated that it was all about the StatePattern, that was true. But the StatePattern gets us 1/2 the story. We also need to do quite a lot of rendering in each of the states, so how do we do that, well for me that all boils down to separation of concern. We could have had a huge load of code in the TimeLineControl that was run dependant on which state we currently in, but that didn't sit well with me. To this end, what I have come up with is a state visualiser which is a UserControl in its own right. So you will get one visualiser per state, and the state visualiser in most cases will also use even more UserControls to make each UserControl only do a small fraction of the work. It's all about separation of concerns don't you know.

The table below attempts to illustrate how the states map to their state visualiser UserControls, and it also shows what internal UserControls the state visualiser UserControls make use of. We will look at a dissected image of each of these state visualiser which I have annotated, along with how a particular state works with its associated state visualiser UserControl.

Current State State Visualising Control State Visualiser Helper Controls
ViewingDecadesState (Data Class) ViewingDecadesStateControl (UserControl)
  • DecadeView (UserControl) which then makes use of ItemBar (UserControl)
ViewingYearsState (Data Class) ViewingYearsStateControl (UserControl)
  • YearView (UserControl) which then makes use of ItemBar (UserControl)
ViewingMonthsState (Data Class) ViewingMonthsStateControl (UserControl)
  • MonthView (UserControl) which then makes use of ItemBar (UserControl)
ViewingDaysState (Data Class) ViewingDaysStateControl (UserControl)
  • DaysView (UserControl) which then makes use of ItemBar (UserControl)
ViewingSpecificDayState (Data Class) ViewingSpecificDayStateControl (UserControl)
  • SpecificDayView (UserControl)

To fully understand this, let us examine some dissected image of these state controls.

ViewingDecadesStateControl

ViewingYearsStateControl

ViewingMonthsStateControl

ViewingDaysStateControl

ViewingSpecificDayStateControl

Following A Single State All The Way Through (Yes Start To Finish)

Ok so now that you can see how each state has a UserControl that is responsible for rendering the UI elements associated with the current state, how about we examine one of these states a bit more carefully. Let's pick one, say the ViewingDaysState. As we stated earlier there are a number of states, and each state inherits from BaseState and implements the IState interface.

The BaseState looks like this:

public abstract class BaseState
{
    public IStateControl StateVisualiser { get; set; }
    public abstract List<ITimeLineItem> TimeLineItems { get; }
    public NavigateArgs NavigateTo { get; set; }
    public void Refresh()
    {
        if (((UserControl)StateVisualiser).IsLoaded)
            StateVisualiser.ReDraw(TimeLineItems);
    }
}

Whilst the IState interface looks like this:

public interface IState
{
    void NavigateDown(TimeLineControl context);
    void NavigateUp(TimeLineControl context);
    IStateControl StateVisualiser { get; }
    void Refresh();
}

So what happens is that when the user chooses to navigate down or up, the TimeLineControl is able to tell its active state that it needs to navigate up or down dependant on what the user did.

This all translates to a state that looks like this for the ViewingDaysState code:

Deep Look At ViewingDaysState

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Collections.ObjectModel;

namespace TimeLineControl
{
    /// <summary>
    /// Viewing days state
    /// </summary>
    public class ViewingDaysState : BaseState, IState, IDisposable
    {
        #region Data
        private TimeLineControl context;
        #endregion

        #region Ctor
        public ViewingDaysState(TimeLineControl context, NavigateArgs args)
        {
            this.NavigateTo = args;
            this.context = context;

            base.StateVisualiser = new ViewingDaysStateControl();
            base.StateVisualiser.CurrentViewingDate = NavigateTo.CurrentViewingDate;
            <highlight />base.StateVisualiser.NavigateUpAction = new Action(context.NavigateUp);
            base.StateVisualiser.NavigateDownAction = new Action(context.NavigateDown);
            ((UserControl)base.StateVisualiser).Loaded += ViewingDaysState_Loaded;
        }
        #endregion

        #region Private Methods
        private void ViewingDaysState_Loaded(object sender, RoutedEventArgs e)
        {
            ((UserControl)base.StateVisualiser).Height = context.Height;
            base.StateVisualiser.ItemsDataTemplate = context.ItemsDataTemplate;
            base.StateVisualiser.ReDraw(TimeLineItems);
        }
        #endregion

        #region Public Properties

        public override List<ITimeLineItem> TimeLineItems
        {
            get
            {
                if (context.TimeLineItems == null)
                    return null;

                DateTime dt = base.StateVisualiser.CurrentViewingDate;

                return context.TimeLineItems.Where(
                    tm => tm.ItemTime.Year == dt.Year &&
                          tm.ItemTime.Month == dt.Month).ToList().SortList();
            }
        }
        #endregion

        #region IState Members

        public void NavigateDown(TimeLineControl context)
        {
            context.State = new ViewingSpecificDayState(
                context, 
                new NavigateArgs(context.State.StateVisualiser.CurrentViewingDate,
                    NavigatingToSource.SpecificDay));
        }

        public void NavigateUp(TimeLineControl context)
        {
            context.State = new ViewingMonthsState(
                context,
                new NavigateArgs(context.State.StateVisualiser.CurrentViewingDate,
                    NavigatingToSource.MonthsOfYear));
        }
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            ((UserControl)base.StateVisualiser).Loaded -= ViewingDaysState_Loaded;
        }

        #endregion
    }
}

See how in there is a reference to context which is the TimeLineControl in the IState.NavigateUp()/IState.NavigateDown() methods. Also of note is within the constructor is how the states state visualiser control (after that is the bit the user interacts with not the actual state code itself) is able to communicate directly with the TimeLineControl by using Action delegates, one for the NavigateUp() and one for the NavigateDown() methods. That means when the user does something in the state visualiser control (ViewingDaysStateControl in this case) that requires a new state, these callback Action delegates are called, and the TimeLineControl is able to ask its current state to NavigateUp() or NavigateDown() accordingly. This is what happens in the TimeLineControls code, see how it just calls its current state, and remember these methods are called directly from the current states state visualiser control thanks to the callback Action delegates provided above.

internal void NavigateDown()
{
    State.NavigateDown(this);
}

internal void NavigateUp()
{
    State.NavigateUp(this);
}

The other point is that when the states visualiser is loaded, the ReDraw() method is called which of course is where all the drawing occurs. You will see more on how the drawing occurs when we examine the ViewingDaysStateControls code.

So that is what the state code looks like, but to continue our journey let's now look at how the states visualiser code (ViewingDaysStateControl in this case) is constructed.

Deep Look At ViewingDaysStateControl

The idea here is that it will create a container to place another helper UserControl DaysView in this case, but also provides the 2 callback Action delegates that will in turn tell the TimeLineControl to change its state by using its own NavigateUp() or NavigateDown() methods.

Here is the full XAML for the ViewingDaysStateControl:

<UserControl x:Class="TimeLineControl.ViewingDaysStateControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TimeLineControl"
    HorizontalAlignment="Stretch" VerticalAlignment="Stretch">

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="../../../Resources/AppStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>


    <DockPanel>
        <Grid x:Name="spButtons" HorizontalAlignment="Stretch" 
              DockPanel.Dock="Top" Height="50" >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Button x:Name="btnUp" Style="{StaticResource leftButtonTemplateStyle}" 
                    Click="NavigateUp_Click" />

            <Border BorderThickness="2" 
                    BorderBrush="{StaticResource TopBannerTextBorderBrush}"
                    Grid.Column="1"
                    Width="Auto"
                    Background="{StaticResource TopBannerBackGround}"
                    CornerRadius="5" Height="30" VerticalAlignment="Center"
                    HorizontalAlignment="Right" Margin="0,0,10,0">
                <Label x:Name="lblDetails"  
                   Margin="5,0,5,0"
                   Padding="0"
                   HorizontalAlignment="Center" 
                   HorizontalContentAlignment="Center"
                   VerticalAlignment="Center" 
                   VerticalContentAlignment="Center"
                   Foreground="{StaticResource TopBannerTextForeground}"
                   FontFamily="Tahoma" FontSize="12" 
                   FontWeight="Bold"/>
            </Border>


        </Grid>


        <local:FrictionScrollViewer Style="{StaticResource ScrollViewerStyle}"
            HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <Grid x:Name="grid" HorizontalAlignment="Stretch"/>
        </local:FrictionScrollViewer>

    </DockPanel>
</UserControl>

Nothing that interesting there, except that there is a navigate up Button, and a Grid that will be used to host the rest of the content. So let's have a look at the code behind now shall we.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TimeLineControl
{
    /// <summary>
    /// State visualiser for <c>ViewingDaysState</c>.
    /// This creates a new <c>DaysView</c> passing it the
    /// List of ITimeLineItem to draw
    /// </summary>
    public partial class ViewingDaysStateControl : 
        UserControl, IStateControl, IDisposable
    {
        #region Ctor
        public ViewingDaysStateControl()
        {
            InitializeComponent();
        }
        #endregion

        #region Public Properties
        public Action NavigateUpAction { get; set; }
        public Action NavigateDownAction { get; set; }
        public DateTime CurrentViewingDate { get; set; }
        public TimeLineControl Parent { private get;  set; }


        #region ItemsDataTemplate

        /// <summary>
        /// ItemsDataTemplate Dependency Property
        /// </summary>
        public static readonly DependencyProperty ItemsDataTemplateProperty =
            DependencyProperty.Register("ItemsDataTemplate", typeof(DataTemplate),
            typeof(ViewingDaysStateControl),
                new FrameworkPropertyMetadata((DataTemplate)null,
                    new PropertyChangedCallback(OnItemsDataTemplateChanged)));

        /// <summary>
        /// Gets or sets the ItemsDataTemplate property.  
        /// </summary>
        public DataTemplate ItemsDataTemplate
        {
            get { return (DataTemplate)GetValue(ItemsDataTemplateProperty); }
            set { SetValue(ItemsDataTemplateProperty, value); }
        }

        /// <summary>
        /// Handles changes to the ItemsDataTemplate property.
        /// </summary>
        private static void OnItemsDataTemplateChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {
        }
        #endregion
        #endregion

        #region Public Methods
        public void ReDraw(List<ITimeLineItem> timeLineItems)
        {
            if (timeLineItems != null)
            {

                CreateBreadCrumb();
                grid.Children.Clear();
                grid.Height = this.Height - 50;

                lblDetails.Content = String.Format("1-{0} {1} {2}",
                    CurrentViewingDate.DaysOfMonth(), 
                    DataHelper.GetNameOfMonth(CurrentViewingDate.Month),
                    CurrentViewingDate.Year);

                DaysView dv = new DaysView()
                {
                    ItemsDataTemplate = this.ItemsDataTemplate,
                    Height = this.Height - spButtons.Height,
                    Width = grid.Width,
                    CurrentViewingDate = CurrentViewingDate,
                    TimeLineItems = timeLineItems
                };
                dv.ViewDateEvent += dv_ViewDateEvent;
                dv.Loaded += (s, e) =>
                {
                    dv.Draw();
                };
                grid.Children.Add(dv);
            }
        }
        #endregion

        #region Private Methods
        private void dv_ViewDateEvent(object sender, DateEventArgs e)
        {
            CurrentViewingDate = e.CurrentViewingDate;
            NavigateDownAction();
        }

        private void NavigateUp_Click(object sender, RoutedEventArgs e)
        {
            NavigateUpAction();
        }

        private void CreateBreadCrumb()
        {
            breadCrumbContainer.Children.Clear();
            BreadCrumb bc = new BreadCrumb();
            bc.Parent = this.Parent;
            bc.NavigateArgs = new NavigateArgs(this.CurrentViewingDate, 
                NavigatingToSource.DaysOfMonth);
            breadCrumbContainer.Children.Add(bc);
        }
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            foreach (DaysView dv in grid.Children)
            {
                dv.ViewDateEvent -= dv_ViewDateEvent;
            }
        }

        #endregion
    }
}

You can see that this code does various things, such as hook up events, work out how much space is available to pass through to the DaysView UserControl which will do all the actual day rendering. It's also waiting until the DaysView UserControl has been loaded and then asks it to Draw itself, where internally it makes use of the TimeLineItems property items that was just populated by the ViewingDaysStateControl.

So the next piece in the puzzle is to examine what the DaysView UserControl does. So let's have a look at that now.

Deep Look At DaysView

This UserControl is responsible for rendering days, and here is its full XAML.

<UserControl x:Class="TimeLineControl.DaysView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto" Background="Transparent" Margin="0,0,-2,0">

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="../../../Resources/AppStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>


    <Grid x:Name="grid">
            
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="35"/>
        </Grid.RowDefinitions>

    </Grid>
</UserControl>

Not much there is there, that's because it's all done in code behind, after all there is some dynamic-ness that we need to deal with, not all months have the same number of days so we need to do that work in code. Here is the code behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;

namespace TimeLineControl
{
    /// <summary>
    /// Displays days <c>ITemLineItem</c>s
    /// </summary>
    public partial class DaysView : UserControl
    {
        #region Data
        private Dictionary<Int32, List<ITimeLineItem>> itemsByKey = 
            new Dictionary<int, List<ITimeLineItem>>();
        private Boolean initialised = false;
        private Int32 daysInCurrentMonth = 0;
        #endregion

        #region Ctor
        public DaysView()
        {
            InitializeComponent();
        }
        #endregion

        #region Public Properties

        public DateTime CurrentViewingDate { get; set; }

        #region ItemsDataTemplate

        /// <summary>
        /// ItemsDataTemplate Dependency Property
        /// </summary>
        public static readonly DependencyProperty ItemsDataTemplateProperty =
            DependencyProperty.Register("ItemsDataTemplate", typeof(DataTemplate),
            typeof(DaysView),
                new FrameworkPropertyMetadata((DataTemplate)null,
                    new PropertyChangedCallback(OnItemsDataTemplateChanged)));

        /// <summary>
        /// Gets or sets the ItemsDataTemplate property.  
        /// </summary>
        public DataTemplate ItemsDataTemplate
        {
            get { return (DataTemplate)GetValue(ItemsDataTemplateProperty); }
            set { SetValue(ItemsDataTemplateProperty, value); }
        }

        /// <summary>
        /// Handles changes to the ItemsDataTemplate property.
        /// </summary>
        private static void OnItemsDataTemplateChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {
        }
        #endregion

        #region TimeLineItems

        /// <summary>
        /// TimeLineItems Dependency Property
        /// </summary>
        public static readonly DependencyProperty TimeLineItemsProperty =
            DependencyProperty.Register("TimeLineItems", typeof(List<ITimeLineItem>),
            typeof(DaysView),
                new FrameworkPropertyMetadata((List<ITimeLineItem>)null,
                    new PropertyChangedCallback(OnTimeLineItemsChanged)));

        /// <summary>
        /// Gets or sets the TimeLineItems property.  
        /// </summary>
        public List<ITimeLineItem> TimeLineItems
        {
            get { return (List<ITimeLineItem>)GetValue(TimeLineItemsProperty); }
            set { SetValue(TimeLineItemsProperty, value); }
        }

        /// <summary>
        /// Handles changes to the TimeLineItems property.
        /// </summary>
        private static void OnTimeLineItemsChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {

        }
        #endregion

        #endregion

        #region Public Methods

        public void Draw()
        {
            if (TimeLineItems.Count() == 0)
                return;

            if (initialised)
                return;

            daysInCurrentMonth = CurrentViewingDate.DaysOfMonth();

            Double barWidth = (this.ActualWidth / daysInCurrentMonth) > 25 ? 
                this.ActualWidth / daysInCurrentMonth : 25;


            //create grid columns for Month
            for (int dayOfMonth = 0; dayOfMonth < daysInCurrentMonth; dayOfMonth++)
            {
                grid.ColumnDefinitions.Add(new ColumnDefinition()
                    {
                        Width = new GridLength(barWidth, GridUnitType.Pixel)
                    });
            }

            grid.Children.Clear();
            Int32 barHeightToDrawIn = (Int32)this.Height - 35;

            Int32 maximumItemsForGraphAcrossAllBars = 0;

            for (int dayOfMonth = 1; dayOfMonth <= daysInCurrentMonth; dayOfMonth++)
            {
                List<ITimeLineItem> items = 
                    (from t in TimeLineItems
                     where t.ItemTime.Year == CurrentViewingDate.Year &&
                           t.ItemTime.Month == CurrentViewingDate.Month &&
                           t.ItemTime.Day == dayOfMonth
                     select t).ToList();
                if (items != null && items.Count > 0)
                    itemsByKey.Add(dayOfMonth, items);
            }

            if (itemsByKey.Count > 0)
                maximumItemsForGraphAcrossAllBars =
                    (from x in itemsByKey
                     select x.Value.Count).Max();

            for (int dayOfMonth = 1; dayOfMonth <= 
                daysInCurrentMonth; dayOfMonth++)
            {
                List<ITimeLineItem> items = null;

                Double columnWidth = grid.ActualWidth / 10;

                if (itemsByKey.TryGetValue(dayOfMonth, out items))
                {
                    ItemsBar bar = new ItemsBar();
                    bar.ItemsDataTemplate = this.ItemsDataTemplate;
                    bar.Height = barHeightToDrawIn;
                    bar.Width = columnWidth;
                    <highlight />bar.TimeLineItems = items;
                    bar.MaximumItemsForGraphAcrossAllBars = 
                        maximumItemsForGraphAcrossAllBars;
                    bar.Loaded += (s, e) =>
                    {
                        bar.Draw();
                    };
                    bar.SetValue(Grid.ColumnProperty, dayOfMonth - 1);
                    bar.SetValue(Grid.RowProperty, 0);
                    grid.Children.Add(bar);
                }

                //Need to build up buttons now
                grid.Children.Add(CreateButton(dayOfMonth, dayOfMonth - 1));
            }

            initialised = true;
        }

        #endregion

        #region Events

        public event EventHandler<DateEventArgs> ViewDateEvent;
        #endregion

        #region Private Methods
        private void OnViewDateEvent(DateTime currentViewingDate)
        {
            // Copy to a temporary variable to be thread-safe.
            EventHandler<DateEventArgs> temp = ViewDateEvent;
            if (temp != null)
                temp(this, new DateEventArgs(currentViewingDate));
        }
        
        private Button CreateButton(Int32 dayOfMonth, Int32 column)
        {
            Button btn = new Button();
            btn.Content = dayOfMonth;
            Style style = (Style)this.Resources["graphSectionButtonStyle"];
            if (style != null) { btn.Style = style; }
            btn.Click += Button_Click;
            btn.Tag = new DateTime(CurrentViewingDate.Year, 
                CurrentViewingDate.Month, dayOfMonth);
            btn.IsEnabled = itemsByKey.ContainsKey(dayOfMonth);
            btn.SetValue(Grid.RowProperty, 1);
            btn.SetValue(Grid.ColumnProperty, column);
            return btn;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Button btn = (Button)sender;
            OnViewDateEvent((DateTime)btn.Tag);
        }
        #endregion
    }
}

It can be seen that this code is largely concerned with setting up the right number of Grid.Columns for the correct number of days in the current month, and also creating a Button whose Tag property stores a particular DateTime. The idea being that is when the Button is clicked the DateTime stored in its Tag property can be used to navigate down to the next state.

There is one final piece to the puzzle which is how the actual bars are rendered. Again this is all separation of concern stuff, since the rendering of the bars is common to all states, it made sense to move that into a common UserControl, which I have called ItemBar.

Deep Look At ItemBar

ItemBar is responsible for rendering a List<ITimeLineItem> based objects (which are populated by some parent control, as can be seen above in the DaysView code behind logic), as I stated this is common to all states. The ItemBar UserControl is slightly clever in the fact that if it does not have enough vertical height to render all the items it will render a single block containing all the items. The ItemBar UserControl also has a Popup window from which the user can view and select an item from the items that make up the bar.

Here is the entire XAML for the ItemBar UserControl.

<UserControl x:Class="TimeLineControl.ItemsBar"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TimeLineControl"
    Height="Auto" Width="Auto">

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="../../Resources/AppStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>

    <Grid >
        <Canvas x:Name="canv" VerticalAlignment="Stretch" 
                HorizontalAlignment="Stretch"/>

        <Popup x:Name="pop" Width="350" Height="190"
               Placement="RelativePoint" AllowsTransparency="True"
               StaysOpen="true"
               PopupAnimation="Scroll"
               VerticalOffset="-40"
               HorizontalOffset="0">
            <Border Background="{StaticResource itemsPopupBackgroundColor}"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    BorderBrush="{StaticResource itemsPopupBorderBrushColor}" 
                    BorderThickness="3" 
                    CornerRadius="5,5,5,5">

                <Grid Background="{StaticResource itemsPopupBackgroundColor}">

                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>

                    <Thumb Grid.Row="0" Width="Auto" Height="40" 
                          Tag="{Binding ElementName=pop}">
                        <Thumb.Template>
                            <ControlTemplate>
                                <Border  Width="Auto" Height="40" 
                                         BorderThickness="0"
                                         Background="{StaticResource 
                                            itemsPopupHeaderBackgroundColor}" 
                                         VerticalAlignment="Top" 
                                         CornerRadius="0" Margin="0">

                                    <Grid HorizontalAlignment="Stretch" 
                                          VerticalAlignment="Stretch">
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="*"/>
                                            <ColumnDefinition Width="Auto"/>
                                        </Grid.ColumnDefinitions>

                                        <StackPanel Grid.Column="0"
                                                Orientation="Horizontal" 
                                                HorizontalAlignment="Stretch" 
                                                VerticalAlignment="Center">

                                            <Label Content="Items"
                                               FontFamily="Tahoma"
                                               FontSize="14"
                                               FontWeight="Bold"
                                               Foreground="{StaticResource 
                                                itemsPopupTitleColor}"
                                               VerticalContentAlignment="Center"
                                               Margin="5,0,0,0" />
                          
                                        </StackPanel>

                                        <Button Grid.Column="1" 
                                                Style="{StaticResource 
                                                    itemsPopupCloseButtonStyle}"
                                                Tag="{Binding ElementName=pop}" 
                                                Margin="5"
                                                Click="HidePopup_Click" />
                                    </Grid>
                                </Border>
                            </ControlTemplate>
                        </Thumb.Template>
                    </Thumb>

                    <ListBox x:Name="lst" Grid.Row="1" 
                         Style="{StaticResource itemsListBoxStyle}" 
                         SelectionChanged="lst_SelectionChanged">
                    </ListBox>
                </Grid>
            </Border>
        </Popup>
    </Grid>
</UserControl>

And here is the code behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;

namespace TimeLineControl
{
    //Event delegate
    public delegate void TimeLineItemClickedEventHandler(object sender, TimeLineArgs e);

    /// <summary>
    /// Represents a single collection of bars that
    /// is used across all state visualiser controls
    /// </summary>
    public partial class ItemsBar : UserControl, IDisposable
    {
        #region Data
        private Brush[] barBrushes = new Brush[] 
        {
            Brushes.CornflowerBlue,
            Brushes.Bisque,
            Brushes.LightSeaGreen,
            Brushes.Coral,
            Brushes.DarkGreen,
            Brushes.DarkSalmon,
            Brushes.Gray,
            Brushes.Goldenrod,
            Brushes.DeepSkyBlue,
            Brushes.Lavender
        };
        #endregion

        #region Ctor
        public ItemsBar()
        {
            InitializeComponent();
        }
        #endregion

        #region Events

        //The actual event routing
        public static readonly RoutedEvent TimeLineItemClickedEvent =
            EventManager.RegisterRoutedEvent(
            "TimeLineItemClicked", RoutingStrategy.Bubble,
            typeof(TimeLineItemClickedEventHandler),
            typeof(ItemsBar));

        //add remove handlers
        public event TimeLineItemClickedEventHandler TimeLineItemClicked
        {
            add { AddHandler(TimeLineItemClickedEvent, value); }
            remove { RemoveHandler(TimeLineItemClickedEvent, value); }
        }
        #endregion

        #region Properties

        public Int32 MaximumItemsForGraphAcrossAllBars { get; set; }

        #region ItemsDataTemplate

        /// <summary>
        /// ItemsDataTemplate Dependency Property
        /// </summary>
        public static readonly DependencyProperty ItemsDataTemplateProperty =
            DependencyProperty.Register("ItemsDataTemplate", typeof(DataTemplate),
            typeof(ItemsBar),
                new FrameworkPropertyMetadata((DataTemplate)null,
                    new PropertyChangedCallback(OnItemsDataTemplateChanged)));

        /// <summary>
        /// Gets or sets the ItemsDataTemplate property.  
        /// </summary>
        public DataTemplate ItemsDataTemplate
        {
            get { return (DataTemplate)GetValue(ItemsDataTemplateProperty); }
            set { SetValue(ItemsDataTemplateProperty, value); }
        }

        /// <summary>
        /// Handles changes to the ItemsDataTemplate property.
        /// </summary>
        private static void OnItemsDataTemplateChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {
            ItemsBar ib = (ItemsBar)d;
            ib.lst.ItemTemplate = (DataTemplate)e.NewValue;
        }
        #endregion

        #region TimeLineItems

        /// <summary>
        /// TimeLineItems Dependency Property
        /// </summary>
        public static readonly DependencyProperty TimeLineItemsProperty =
            DependencyProperty.Register("TimeLineItems", typeof(List<ITimeLineItem>),
            typeof(ItemsBar),
                new FrameworkPropertyMetadata((List<ITimeLineItem>)null,
                    new PropertyChangedCallback(OnTimeLineItemsChanged)));

        /// <summary>
        /// Gets or sets the TimeLineItems property.  
        /// </summary>
        public List<ITimeLineItem> TimeLineItems
        {
            get { return (List<ITimeLineItem>)GetValue(TimeLineItemsProperty); }
            set { SetValue(TimeLineItemsProperty, value); }
        }

        /// <summary>
        /// Handles changes to the TimeLineItems property.
        /// </summary>
        private static void OnTimeLineItemsChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {

        }
        #endregion

        #endregion

        #region Private/Internal Methods

        internal void Draw()
        {
            canv.Children.Clear();
            canv.Height = this.Height;
            if (this.TimeLineItems.Count * 
                TimeLineControl.PREFERRED_ITEM_HEIGHT <= this.Height)
            {
                for (int i = 0; i < TimeLineItems.Count; i++)
                {
                    Rectangle r = new Rectangle();
                    r.Height = TimeLineControl.PREFERRED_ITEM_HEIGHT;
                    r.Width = this.Width-2;
                    r.HorizontalAlignment = HorizontalAlignment.Center;
                    r.Fill = barBrushes[i % barBrushes.Length];
                    r.SetValue(Canvas.LeftProperty, (Double)1.0);
                    r.SetValue(Canvas.BottomProperty, (Double)(i * r.Height));
                    r.Tag = TimeLineItems[i];
                    r.ToolTip = string.Format("Description {0}\r\nItemTime : {1}",
                        TimeLineItems[i].Description, TimeLineItems[i].ItemTime);
                    r.MouseDown += Item_MouseDown;
                    canv.Children.Add(r);
                }
            }
            else
            {
                double heightForBar = this.Height;
                if (TimeLineItems.Count < MaximumItemsForGraphAcrossAllBars)
                {
                    double percentageOfMax = 
                        (TimeLineItems.Count / 
                            MaximumItemsForGraphAcrossAllBars) * 100;
                    heightForBar = (this.Height / 100) * percentageOfMax;
                }

                Rectangle r = new Rectangle();
                r.Height = heightForBar;
                r.Width = this.Width - 2;
                r.HorizontalAlignment = HorizontalAlignment.Center;
                r.Fill = barBrushes[0];
                r.SetValue(Canvas.LeftProperty, (Double)1.0);
                r.SetValue(Canvas.BottomProperty, (Double)0.0);
                r.Tag = TimeLineItems;
                r.ToolTip = string.Format("Collection of timeLines" + 
                    "\r\nTo many to show individually");
                r.MouseDown += Item_MouseDown;
                canv.Children.Add(r);
            }
        }

        private void Item_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (pop.IsOpen)
                return;

            Rectangle rect = (Rectangle)sender;
            IEnumerable<ITimeLineItem> itemForRect = null;

            if (rect.Tag is ITimeLineItem)
            {
                List<ITimeLineItem> items = new List<ITimeLineItem>();
                items.Add((ITimeLineItem)rect.Tag);
                itemForRect = items.AsEnumerable();
            }

            if (rect.Tag is IEnumerable<ITimeLineItem>)
            {
                itemForRect = (IEnumerable<ITimeLineItem>)rect.Tag;
            }

            if (itemForRect != null)
            {
                lst.ItemsSource = itemForRect;
                pop.IsOpen = true;
            }
        }

        private void HidePopup_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                Popup popup = (Popup)((Button)sender).Tag;
                popup.IsOpen = false;
            }
            catch
            {
            }
        }

        private void lst_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            pop.IsOpen = false;
            //raise event
            TimeLineArgs args = new TimeLineArgs(
                ItemsBar.TimeLineItemClickedEvent, 
                (ITimeLineItem)lst.SelectedItem);
            RaiseEvent(args);
        }

        private void btnClosePopup_Click(object sender, RoutedEventArgs e)
        {
            pop.IsOpen = false;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (canv.Children.Count > 0)
            {
                foreach (Rectangle rect in canv.Children)
                {
                    rect.MouseDown -= Item_MouseDown;
                }
            }
        }

        #endregion
    }
}

And there we have it, that is how one state works entirely.

But What About The Other States

The other states are all variations on this, the only difference is that the List<ITimeLineItem> supplied to each state visualiser UserControl will be only those needed to display what the user selected via their navigation route.

How To Use It In Your Own Project

I have tried to make it as simple as possible for you to use the attached TimeLineControl in your own application. All you really need to do is follow these 3 steps. I shall show an example of each of these 3 steps as we go.

Step 1: Create A TimeLineControl.ITimeLine Implementing Data Class

The TimeLineControl attached to this article is expecting certain type of items to be provided before it can render anything. These items must implement the TimeLineControl.ITimeLineItem interface, which if you recall looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TimeLineControl
{
    /// <summary>
    /// Expected interface that must be implemented
    /// to add items to the <c>TimeLineControl</c>
    /// </summary>
    public interface ITimeLineItem
    {
        String Description { get; set; }
        DateTime ItemTime { get; set; }
    }
}

So all YOU need to do is implement this TimeLineControl.ITimeLineItem interface on some sort of data class. Here is an example (Note this example also shows you how to use Image(s) in your TimeLineControl.ITimeLineItem implementing data classes. You can see that a full pack syntax URL is required that is because you are using a control (TimeLineControl) which is in a different assembly, and then passing it a DataTemplate that may or may not want to display Image(s). So if you choose to display Image(s) in your TimeLineControl.ITimeLineItem implementing data classes, you MUST fully qualify their locations using the pack syntax so that the TimeLineControl knows how to display them when the time comes. Anyway we kind of went off track there, here is what a TimeLineControl.ITimeLineItem implementing a data class looks like that also supports images:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TimeLineControl;
using System.Windows.Media.Imaging;
using System.Diagnostics;

namespace TimeLineDemoProject
{
    /// <summary>
    /// Simple <c>TimeLineControl.ITimeLineItem</c> implementation
    /// which can be used within a DataTemplate that is then supplied
    /// to the <c>TimeLineControl</c>
    /// </summary>
    [DebuggerDisplay("{ToString()}")]
    public class DataTimeLineItem : INPCBase, ITimeLineItem
    {
        #region Data
        private String description;
        private DateTime itemTime;
        private static BitmapImage descriptionImage;
        private static BitmapImage itemTimeImage;
        #endregion

        #region Ctor
        public DataTimeLineItem(String description, DateTime itemTime)
        {
            this.description = description;
            this.itemTime = itemTime;
        }
        #endregion

        #region Public Properties
        public String Description
        {
            get { return description; }
            set
            {
                description = value;
                RaisePropertyChanged("Description");
            }
        }

        /// <summary>
        /// As <c>TimeLineControl</c> is in different assembly
        /// and this will be used inside DataTemplate in this 
        /// project, need full url so <c>TimeLineControl</c>
        /// knows how to display images
        /// </summary>
        public BitmapImage DescriptionImage
        {
            get
            {
                if (descriptionImage == null)
                {
                    descriptionImage = new BitmapImage(
                        new Uri("pack://application:,,,/" +
                            "TimeLineDemoProject;component/Images/description.png"));
                }
                return descriptionImage;
            }
        }
        
        public DateTime ItemTime
        {
            get { return itemTime; }
            set
            {
                itemTime = value;
                RaisePropertyChanged("ItemTime");
            }
        }

        /// <summary>
        /// As <c>TimeLineControl</c> is in different assembly
        /// and this will be used inside DataTemplate in this 
        /// project, need full url so <c>TimeLineControl</c>
        /// knows how to display images
        /// </summary>
        public BitmapImage ItemTimeImage
        {
            get
            {
                if (itemTimeImage == null)
                {
                    itemTimeImage = new BitmapImage(
                        new Uri("pack://application:,,,/" +
                            "TimeLineDemoProject;component/Images/itemtime.png"));
                }
                return itemTimeImage;
            }
        }
        #endregion

        #region Overrides
        public override string ToString()
        {
            return String.Format("Description : {0}\r\n ItemTime : {1}", 
                Description, ItemTime);
        }
        #endregion
    }
}

Step 2 : Create A List Of Your TimeLineControl.ITimeLineItem Implementing Data Class, And Use Them Within The TimeLineControl

Obviously the TimeLineControl is going to want to display some items (where the items are expected to be TimeLineControl.ITimeLineItem implementing classes). So how to we get the TimeLineControl to use some items. It is very simple - the TimeLineControl has a DependencyProperty called TimeLineItems which is expecting a ObservableList<ITimeLineItem>. So all you need to do is populate that property either using MVVM and Databinding or from code behind if you prefer. I have included a simple ViewModel in the attached code which creates a ObservableList<ITimeLineItem>, which looks like this:

using System;
using System.Collections.ObjectModel;
using TimeLineControl;

namespace TimeLineDemoProject
{
    /// <summary>
    /// Simple dummy ViewModel
    /// </summary>
    public class Window1ViewModel : INPCBase
    {
        #region Data
        private ObservableCollection<ITimeLineItem> timeItems;
        #endregion

        #region Ctor
        public Window1ViewModel()
        {
            LoadItems();
        }
        #endregion

        #region Public Properties
        public ObservableCollection<ITimeLineItem> TimeItems
        {
            get
            {
                return timeItems;
            }
        }
        #endregion

        #region Private Methods
        private void LoadItems()
        {
            timeItems = new ObservableCollection<ITimeLineItem>();

            //simulate fetching these from web server or something
            timeItems.Add(new DataTimeLineItem("This is 1995 Month 12, Day 1", 
                new DateTime(1995, 12, 1)));
            timeItems.Add(new DataTimeLineItem("This is 1995 Month 12, Day 2", 
                new DateTime(1995, 12, 2)));
            timeItems.Add(new DataTimeLineItem("This is 1995 Month 12, Day 3", 
                new DateTime(1995, 12, 3)));
        }
        #endregion
    }
}

And all that then needs to be done is to pass this ObservableList<ITimeLineItem> to the TimeLineControl in XAML like so.

<timeline:TimeLineControl 
	TimeLineItems="{Binding TimeItems}">

</timeline:TimeLineControl>

As I say though, this could be all done using code behind too, I am not pushing MVVM on you, I just happen to like it.

Step 3 : Supply A DataTemplate For Your TimeLineControl.ITimeLineItem Implementing Data Class

Internally the TimeLineControl is displaying your TimeLineControl.ITimeLineItem implementing data classes within a ListBox, so we can use that to our advantage and allow the user to specify what the data should look like. This is easily achieved using a simple DataTemplate, which should match your own TimeLineControl.ITimeLineItem implementing data class details. This can be as simple or as crazy as you like. The attached code has quite an elaborate DataTemplate, because I like things to look nice. But it could be as simple as you like. To understand this, this is how we would provide a custom DataTemplate to the TimeLineControl.

<Window.Resources>

    <DataTemplate x:Key="timeDataTemplate" DataType="{x:Type local:DataTimeLineItem}">

	<!-- Left out for clarity -->	
	<!-- Left out for clarity -->	
	<!-- Left out for clarity -->	
	<!-- Left out for clarity -->	
	<!-- Left out for clarity -->	

    </DataTemplate>


</Window.Resources>

<timeline:TimeLineControl 
      ItemsDataTemplate="{StaticResource timeDataTemplate}">

</timeline:TimeLineControl>

The demo app has a rather complicated DataTemplate which is defined as follows:

<DataTemplate x:Key="timeDataTemplate" 
	DataType="{x:Type local:DataTimeLineItem}">

    <DataTemplate.Resources>
        <Storyboard x:Key="Timeline1">
            <DoubleAnimationUsingKeyFrames 
                BeginTime="00:00:00" 
                Storyboard.TargetName="glow" 
                Storyboard.TargetProperty="(UIElement.Opacity)">
                <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="Timeline2">
            <DoubleAnimationUsingKeyFrames 
                BeginTime="00:00:00" 
                Storyboard.TargetName="glow" 
                Storyboard.TargetProperty="(UIElement.Opacity)">
                <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
    </DataTemplate.Resources>

    <Border x:Name="bord" Margin="10" 
            BorderBrush="#FFFFFFFF" 
            Background="Black" 
            BorderThickness="2" 
            CornerRadius="4,4,4,4">
        <Border Background="#7F000000" 
                BorderBrush="White"
                Margin="-2" 
                BorderThickness="2" 
                CornerRadius="4,4,4,4">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="0.507*"/>
                    <RowDefinition Height="0.493*"/>
                </Grid.RowDefinitions>
                <Border x:Name="glow"  Opacity="0" 
                        HorizontalAlignment="Stretch" 
                        Width="Auto" Grid.RowSpan="2" 
                        CornerRadius="4,4,4,4">
                    <Border.Background>
                        <RadialGradientBrush>
                            <RadialGradientBrush.RelativeTransform>
                                <TransformGroup>
                                    <ScaleTransform ScaleX="1.702" ScaleY="2.243"/>
                                    <SkewTransform AngleX="0" AngleY="0"/>
                                    <RotateTransform Angle="0"/>
                                    <TranslateTransform X="-0.368" Y="-0.152"/>
                                </TransformGroup>
                            </RadialGradientBrush.RelativeTransform>
                            <GradientStop Color="#B28DBDFF" Offset="0"/>
                            <GradientStop Color="#008DBDFF" Offset="1"/>
                        </RadialGradientBrush>
                    </Border.Background>
                </Border>

                <StackPanel Orientation="Vertical" Grid.RowSpan="2"
                            Background="Transparent" 
                            Margin="2">

                    <StackPanel Orientation="Horizontal">
                        <Grid VerticalAlignment="Center" 
                               HorizontalAlignment="Left"
                               Margin="5">
                            <Ellipse Fill="Black" Stroke="White" 
                               StrokeThickness="2"
                               VerticalAlignment="Center" 
                               HorizontalAlignment="Center"
                               Width="25"
                               Height="25"/>

                            <Image VerticalAlignment="Center" 
                               HorizontalAlignment="Center"
                               Source="{Binding DescriptionImage}" 
                               Width="15" Height="15"/>

                        </Grid>
                        <Label Content="Description:" 
                               VerticalAlignment="Center"
                               VerticalContentAlignment="Center"
                               FontSize="13"
                               FontWeight="Bold" 
                               FontFamily="Tahoma" 
                               Foreground="White"/>
                    </StackPanel>

                    <TextBlock Text="{Binding Description}" 
                           FontFamily="Tahoma" 
                           FontSize="10"
                           TextWrapping="Wrap" 
                           Foreground="White"
                           Margin="40,2,0,0"/>

                    <StackPanel Orientation="Horizontal">
                        <Grid VerticalAlignment="Top" 
                               HorizontalAlignment="Left"
                               Margin="5">
                            <Ellipse Fill="Black" Stroke="White"
                               StrokeThickness="2"
                               VerticalAlignment="Center" 
                               HorizontalAlignment="Center"
                               Width="25"
                               Height="25"/>

                            <Image VerticalAlignment="Center" 
                               HorizontalAlignment="Center"
                               Source="{Binding ItemTimeImage}" 
                               Width="15" Height="15"/>

                        </Grid>
                        <Label Content="Item Time:" 
                           VerticalAlignment="Center"
                           VerticalContentAlignment="Center"
                           FontSize="13"
                           FontWeight="Bold" 
                           FontFamily="Tahoma" 
                           Foreground="White"/>
                    </StackPanel>

                    <Label Content="{Binding ItemTime}" 
                           FontFamily="Tahoma" 
                           FontSize="10"
                           Foreground="White"
                           VerticalAlignment="Center" 
                           VerticalContentAlignment="Center"
                           Height="Auto"
                           Margin="35,0,0,0"/>
                </StackPanel>

                <Border  x:Name="shine" HorizontalAlignment="Stretch" 
                         Margin="0,0,0,0" Width="Auto" 
                        CornerRadius="4,4,0,0">
                    <Border.Background>
                        <LinearGradientBrush EndPoint="0.494,0.889" 
                                             StartPoint="0.494,0.028">
                            <GradientStop Color="#99FFFFFF" Offset="0"/>
                            <GradientStop Color="#33FFFFFF" Offset="1"/>
                        </LinearGradientBrush>
                    </Border.Background>
                </Border>
            </Grid>
        </Border>
    </Border>
    <DataTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Trigger.EnterActions>
                <BeginStoryboard Storyboard="{StaticResource Timeline1}"/>
            </Trigger.EnterActions>
            <Trigger.ExitActions>
                <BeginStoryboard x:Name="Timeline2_BeginStoryboard" 
                                 Storyboard="{StaticResource Timeline2}"/>
            </Trigger.ExitActions>
        </Trigger>
    </DataTemplate.Triggers>
</DataTemplate>

Which when used looks like this within the TimeLineControl:

But you could make a much simpler DataTemplate, something like this:

<DataTemplate x:Key="SimpleTimeDataTemplate" 
              DataType="{x:Type local:DataTimeLineItem}">

    <StackPanel Orientation="Vertical" 
                            Background="Pink" 
                            Margin="2">

        <Label Content="Description:" 
                           VerticalAlignment="Center"
                           VerticalContentAlignment="Center"
                           FontSize="13"
                           FontWeight="Bold" 
                           FontFamily="Tahoma" 
                           Foreground="White"/>

        <TextBlock Text="{Binding Description}" 
                           FontFamily="Tahoma" 
                           FontSize="10"
                           TextWrapping="Wrap" 
                           Foreground="White"
                           Margin="0,2,0,0"/>

        <Label Content="Item Time:" 
                           VerticalAlignment="Center"
                           VerticalContentAlignment="Center"
                           FontSize="13"
                           FontWeight="Bold" 
                           FontFamily="Tahoma" 
                           Foreground="White"/>

        <Label Content="{Binding ItemTime}" 
                           FontFamily="Tahoma" 
                           FontSize="10"
                           Foreground="White"
                           VerticalAlignment="Center" 
                           VerticalContentAlignment="Center"
                           Height="Auto"
                           Margin="0,0,0,0"/>

    </StackPanel>
</DataTemplate>

Which when used looks like this within the TimeLineControl:

See how easy it is to get a custom DataTemplate for your items into the TimeLineControl.

How To Re-Style It, If You Do Not Like The Cut Of My Jib

For any reason, if you do not dig what I have done and consider me to be Dr Frankenstein and his crazy monster app, ALL YOU HAVE TO DO IS modify the Styles/ControlTemplates in the file TimeLineControl\Resources\AppStyles.xaml. That file is basically a ResourceDictionary that contains ALL the Style(s) for the attached TimeLineControl.

I know I could have made everyone's lives easier by using ComponentResourceKey, which you can read more about here.

The thing is I do this stuff for free and for fun, and I am not a one man component vendor, so sometimes I will be pragmatic and take the path of least resistance, which in my case equates to a simple ResourceDictionary.

I find that is generally enough, people can simply modify the TimeLineControl\Resources\AppStyles.xaml ResourceDictionary file, job done.

Hope that is ok with you folk, sometimes simple is best.

Amendments

Josh Smith left a message stating that he would like to see a breadcrumb in there as well to avoid all the back button clicking. This is now in there, but I could not bring myself to update all the articles images (as I have lost some of the originals) and the video, so please forgive me, a new screen shot of the breadcrumb enabled code looks like this:

You can now easily click back 1 state at a time, as before, or use the BreadCrumb to just back up as many steps as you like. The BreadCrumb is in fact a standalone re-usable UserControl, and the way the BreadCrumb works is pretty simple. An instance of the BreadCrumb UserControl is hosted within each of the state visualisers UserControls, and the BreadCrumb is simply made aware of the parent TimeLineControl, and when a user clicks one of the BreadCrumb buttons, the parent TimeLineControl is told to navigate to the required (possibly new) state.

Anyway here is the entire code for the BreadCrumb (only code behind here, see download for XAML, it's not that relevant) :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TimeLineControl
{
    /// <summary>
    /// Simple breadcrumb of visited states
    /// </summary>
    public partial class BreadCrumb : UserControl
    {
        #region Data
        private NavigateArgs navigateArgs = null;
        #endregion

        #region Public Properties
        public TimeLineControl Parent { private get; set; }
        public NavigateArgs NavigateArgs
        {
            get
            {
                return navigateArgs;
            }
            set
            {
                navigateArgs = value;
                WorkOutWhatToShow();
            }
        }

        #endregion

        #region Ctor
        public BreadCrumb()
        {
            InitializeComponent();
        }
        #endregion

        #region Private Methods
        private void WorkOutWhatToShow()
        {
            switch (NavigateArgs.NavigateTo)
            {
                case NavigatingToSource.Decades:
                    break;
                case NavigatingToSource.YearsOfDecade:
                    CreateCrumbForYears();
                    break;
                case NavigatingToSource.MonthsOfYear:
                    CreateCrumbForMonths();
                    break;
                case NavigatingToSource.DaysOfMonth:
                    CreateCrumbForDays();
                    break;
                case NavigatingToSource.SpecificDay:
                    CreateCrumbForSpecificDay();
                    break;
            }
        }

        private void ShowDecades()
        {
            Parent.State = new ViewingDecadesState(this.Parent, 
                new NavigateArgs(
                    this.NavigateArgs.CurrentViewingDate, 
                    NavigatingToSource.Decades));
        }

        private void ShowYears()
        {
            Parent.State = new ViewingYearsState(this.Parent,
                new NavigateArgs(
                    this.NavigateArgs.CurrentViewingDate,
                    NavigatingToSource.YearsOfDecade));
        }

        private void ShowMonths()
        {
            Parent.State = new ViewingMonthsState(this.Parent,
                new NavigateArgs(
                    this.NavigateArgs.CurrentViewingDate,
                    NavigatingToSource.MonthsOfYear));
        }

        private void ShowDays()
        {
            Parent.State = new ViewingDaysState(this.Parent,
                new NavigateArgs(
                    this.NavigateArgs.CurrentViewingDate,
                    NavigatingToSource.DaysOfMonth));
        }

        private void CreateCrumbForYears()
        {
            Button btnDecades = CreateCrumbButton("Decades", ()=> ShowDecades());
            spBreadCrumb.Children.Add(btnDecades);
            spBreadCrumb.Children.Add(CreateCrumbLabel("Years"));
        }

        private void CreateCrumbForMonths()
        {
            Button btnDecades = CreateCrumbButton("Decades", () => ShowDecades());
            spBreadCrumb.Children.Add(btnDecades);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            Button btnYears = CreateCrumbButton("Years", () => ShowYears());
            spBreadCrumb.Children.Add(btnYears);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            spBreadCrumb.Children.Add(CreateCrumbLabel("Months"));
        }

        private void CreateCrumbForDays()
        {
            Button btnDecades = CreateCrumbButton("Decades", () => ShowDecades());
            spBreadCrumb.Children.Add(btnDecades);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            Button btnYears = CreateCrumbButton("Years", () => ShowYears());
            spBreadCrumb.Children.Add(btnYears);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            Button btnMonths = CreateCrumbButton("Months", () => ShowMonths());
            spBreadCrumb.Children.Add(btnMonths);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            spBreadCrumb.Children.Add(CreateCrumbLabel("Days"));
        } 

        private void CreateCrumbForSpecificDay()
        {
            Button btnDecades = CreateCrumbButton("Decades", () => ShowDecades());
            spBreadCrumb.Children.Add(btnDecades);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            Button btnYears = CreateCrumbButton("Years", () => ShowYears());
            spBreadCrumb.Children.Add(btnYears);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            Button btnMonths = CreateCrumbButton("Months", () => ShowMonths());
            spBreadCrumb.Children.Add(btnMonths);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            Button btnDays = CreateCrumbButton("Days", () => ShowDays());
            spBreadCrumb.Children.Add(btnDays);
            spBreadCrumb.Children.Add(CreateCrumbLabel(">"));
            spBreadCrumb.Children.Add(CreateCrumbLabel("Current"));
        }

        private Button CreateCrumbButton(String text, Action workToDo)
        {
            Button btn = new Button();
            btn.Content = text;
            btn.ToolTip = text;
            btn.Click += (s, e) => workToDo();
            btn.Style = this.Resources["crumbButton"] as Style;
            return btn;
        }

        private Label CreateCrumbLabel(String content)
        {
            Label lbl = new Label();
            lbl.Content = content;
            lbl.Style = this.Resources["crumbLabel"] as Style;
            return lbl;
        }
        #endregion
    }
}

Known Issues

The TimeLineControl does not cater for Height runtime resizing. This is a known limitation, you must specify a Height for the TimeLineControl when you use it in your own XAML or code. Like this in XAML:

<timeline:TimeLineControl 
	Height="250" 
</timeline:TimeLineControl>

Or like this in code behind:

timeItems.Height = 250;

That's It, Folks

Anyways folks, that is all I have to say for now. Although at its core, this article is a very simple idea, I am really pleased with the results, and do think it's really easy to use in your own project. As such, I sure would really appreciate some votes, and some comments if you feel this control will help you out in your own WPF projects. As I stated in the introduction, this control has actually turned out to be one of the most complicated (well not including work ones) that I have had the pleasure of writing (even though it looks simple), so get your votes in if you appreciate the work I do.

History

  • 14th April, 2010: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralRe: Great stuff as usual PinmvpSacha Barber14-Apr-10 10:02 
GeneralRe: Great stuff as usual PinmvpSacha Barber14-Apr-10 11:46 
GeneralRe: Great stuff as usual PinmvpNishant Sivakumar15-Apr-10 11:05 
GeneralRe: Great stuff as usual PinmvpSacha Barber15-Apr-10 20:14 
GeneralBreadcrumb trail PinmvpJosh Smith14-Apr-10 5:33 
GeneralRe: Breadcrumb trail PinmvpSacha Barber14-Apr-10 6:46 
GeneralRe: Breadcrumb trail PinmvpSacha Barber14-Apr-10 11:41 
GeneralRe: Breadcrumb trail PinmvpJosh Smith14-Apr-10 11:57 
Nice!
:josh:
Advanced MVVM[^]
Advance your MVVM skills

GeneralRe: Breadcrumb trail PinmvpSacha Barber14-Apr-10 19:23 
GeneralNice Pinmembersam.hill14-Apr-10 5:22 
GeneralRe: Nice PinmemberMDL=>Moshu14-Apr-10 6:23 
GeneralRe: Nice PinmvpSacha Barber14-Apr-10 6:49 
GeneralRe: Nice PinmvpSacha Barber14-Apr-10 6:57 
GeneralI can't believe it!!! - The same day! Are you trying to promote yourself for Votes!!! PinmemberAlan Beasley14-Apr-10 5:18 
GeneralRe: I can't believe it!!! - The same day! Are you trying to promote yourself for Votes!!! PinmvpSacha Barber14-Apr-10 6:41 
GeneralRe: I can't believe it!!! - The same day! Are you trying to promote yourself for Votes!!! PinmemberAlan Beasley14-Apr-10 7:07 
GeneralRe: I can't believe it!!! - The same day! Are you trying to promote yourself for Votes!!! PinmvpSacha Barber14-Apr-10 8:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140709.1 | Last Updated 14 Apr 2010
Article Copyright 2010 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid