Table Of Contents
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.
There is really only one pre-requisite for this control, which is VS2008 or VS2010 if you have it.
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.
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.
The next couple of sections will outline the overall internal design and talk you through how it works.
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:
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
{
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.
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.
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 UserControl
s 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 UserControl
s, and it also shows what internal UserControl
s the state visualiser UserControl
s 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
{
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;
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 TimeLineControl
s 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 ViewingDaysStateControl
s 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
{
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
public static readonly DependencyProperty ItemsDataTemplateProperty =
DependencyProperty.Register("ItemsDataTemplate", typeof(DataTemplate),
typeof(ViewingDaysStateControl),
new FrameworkPropertyMetadata((DataTemplate)null,
new PropertyChangedCallback(OnItemsDataTemplateChanged)));
public DataTemplate ItemsDataTemplate
{
get { return (DataTemplate)GetValue(ItemsDataTemplateProperty); }
set { SetValue(ItemsDataTemplateProperty, value); }
}
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
{
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
public static readonly DependencyProperty ItemsDataTemplateProperty =
DependencyProperty.Register("ItemsDataTemplate", typeof(DataTemplate),
typeof(DaysView),
new FrameworkPropertyMetadata((DataTemplate)null,
new PropertyChangedCallback(OnItemsDataTemplateChanged)));
public DataTemplate ItemsDataTemplate
{
get { return (DataTemplate)GetValue(ItemsDataTemplateProperty); }
set { SetValue(ItemsDataTemplateProperty, value); }
}
private static void OnItemsDataTemplateChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
}
#endregion
#region TimeLineItems
public static readonly DependencyProperty TimeLineItemsProperty =
DependencyProperty.Register("TimeLineItems", typeof(List<ITimeLineItem>),
typeof(DaysView),
new FrameworkPropertyMetadata((List<ITimeLineItem>)null,
new PropertyChangedCallback(OnTimeLineItemsChanged)));
public List<ITimeLineItem> TimeLineItems
{
get { return (List<ITimeLineItem>)GetValue(TimeLineItemsProperty); }
set { SetValue(TimeLineItemsProperty, value); }
}
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;
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;
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);
}
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)
{
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
{
public delegate void TimeLineItemClickedEventHandler(object sender, TimeLineArgs e);
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
public static readonly RoutedEvent TimeLineItemClickedEvent =
EventManager.RegisterRoutedEvent(
"TimeLineItemClicked", RoutingStrategy.Bubble,
typeof(TimeLineItemClickedEventHandler),
typeof(ItemsBar));
public event TimeLineItemClickedEventHandler TimeLineItemClicked
{
add { AddHandler(TimeLineItemClickedEvent, value); }
remove { RemoveHandler(TimeLineItemClickedEvent, value); }
}
#endregion
#region Properties
public Int32 MaximumItemsForGraphAcrossAllBars { get; set; }
#region ItemsDataTemplate
public static readonly DependencyProperty ItemsDataTemplateProperty =
DependencyProperty.Register("ItemsDataTemplate", typeof(DataTemplate),
typeof(ItemsBar),
new FrameworkPropertyMetadata((DataTemplate)null,
new PropertyChangedCallback(OnItemsDataTemplateChanged)));
public DataTemplate ItemsDataTemplate
{
get { return (DataTemplate)GetValue(ItemsDataTemplateProperty); }
set { SetValue(ItemsDataTemplateProperty, value); }
}
private static void OnItemsDataTemplateChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ItemsBar ib = (ItemsBar)d;
ib.lst.ItemTemplate = (DataTemplate)e.NewValue;
}
#endregion
#region TimeLineItems
public static readonly DependencyProperty TimeLineItemsProperty =
DependencyProperty.Register("TimeLineItems", typeof(List<ITimeLineItem>),
typeof(ItemsBar),
new FrameworkPropertyMetadata((List<ITimeLineItem>)null,
new PropertyChangedCallback(OnTimeLineItemsChanged)));
public List<ITimeLineItem> TimeLineItems
{
get { return (List<ITimeLineItem>)GetValue(TimeLineItemsProperty); }
set { SetValue(TimeLineItemsProperty, value); }
}
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;
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.
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
{
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
{
[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");
}
}
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");
}
}
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
{
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>();
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}">
-->
-->
-->
-->
-->
</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
.
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 Style
s/ControlTemplate
s 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.
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 UserControl
s, 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
{
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
}
}
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;
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