WPF Timeline Control - Part I

Introduction
This article will go over a tool that gives you the ability to create and manage multiple timelines with freely editable items. You can add items to multiple timelines in a structure that is MVVM compatible. The control manages logic to provide some intelligent behaviors, etc. The presentation is completely open to use your own data templates to allow you to have full control of all aspects of your presentation. All start and end time values are managed through data binding, which allows you to use this control and have real time updates to your own entities.
This is my first foray into control development, so I hope it will be useful to many, but I also really look forward to any feedback and contributions. My project does utilize a very excellent project created by Steve Kirk for drag and drop functionality. His articles can be found here. The project as it appears in my solution has been modified in a few ways to suit some of my needs.
Background
There are a lot of timeline tools out there for WPF, but none of them provide the kind of behavior I needed. I recently launched a project called BookWeaver to help fiction authors manage their writing. One of the big features I wanted to provide was a timeline tool that allows an author to track complex multi-threaded stories. I wanted something that allows a user to freely edit an events start and end times while intelligently preventing overlap on a given line. It needed to manage multiple timelines so that you can track concurrent storylines. The control needed to let users click on a given item and move it in time, effectively pushing other items when it bumps into them. I wanted to let the user edit start or end times by clicking on the left or right edges respectively. I also wanted to let users have a means to keep start and end times connected when editing one that has a contiguous neighbor.
Using the Code
The timeline control requires that all Items implement the ITimeLineDataItem
interface that is defined in the TimeLineTool
project. If you are using the MVVM pattern, then you should have a viewmodel
that houses the observable collections for all of your timelines. So long as it also implements the ITimeLineManager
interface, you can enable and disable a timeline.
public interface ITimeLineDataItem
{
DateTime? StartTime { get; set; }
DateTime? EndTime { get; set; }
Boolean TimelineViewExpanded { get; set; }
}
Notice that the TimeLineControls
in the sample project are housed inside an items control. In a more robust project, you would probably bind an ItemsControl
with a collection of your own. When the Timeline
controls are housed within an ItemsControl
, then the control does a fairly good job of keeping all of the siblings properly synchronized. The following is a real world example of XAML for timelines that are generated using MVVM bindings:
<ItemsControl ItemsSource="{Binding Path=Threads}"
AlternationCount="2"
HorizontalAlignment="Stretch">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
<Setter Property="ContentTemplate"
Value="{StaticResource ThreadTemplate}"></Setter>
</Trigger>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="ContentTemplate"
Value="{StaticResource ThreadAlternateTemplate}"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!--********* ThreadTemplate **********-->
<DataTemplate DataType="{x:Type ViewModels:ThreadViewModel}"
x:Key="ThreadTemplate">
<tt:TimeLineControl Items="{Binding Path=Scenes, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
DrawTimeGrid="True"
Background="{StaticResource NormalBaseColorBrush}"
UnitSize="{Binding ElementName=ThreadCollection_Root,
Path=DataContext.TimeLineUnitSize, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
StartDate="{Binding ElementName=ThreadCollection_Root,
Path=DataContext.ActualStartDate}"
FocusOnItem="{Binding ElementName=ThreadCollection_Root,
Path=DataContext.FocusScene}"
Height="30"
MinWidth="500"
MinimumUnitWidth="20"
ItemTemplate="{StaticResource TimeLineSceneTemplate}"
HorizontalAlignment="Left"
Manager="{Binding}"
Grid.Column="1"
ToolTip="{Binding Path=MyModel.ThreadName.Value}"/>
</DataTemplate>
The TimeLineSceneTemplate
is pretty effectively demonstrated in a simple form in the demo project, so I am not including a real world example of it. You can see a few other properties that you can bind to. FocusScene
lets you tell the control to scroll and zoom as needed to bring a given item into view. You can bind to DrawTimeGrid
, which I would recommend. The grids can slow down performance, so it is actually best to only show them when the user requests them. You can set brushes for various time unit grid lines, or you can use defaults that will be various intensities of Gray/Black.
Points of Interest
It has been a long time since I actually did the implementation of the project, so a lot of the particulars aren't fresh in my memory. One thing I did find particularly challenging was dealing with interdependent dependency properties. Often times, I wanted similar actions to happen when multiple properties changed, but that action depends on all of the properties being initialized. So quite often, I would get exceptions depending on what order things happened. I am not entirely happy with how complicated everything seemed to get, and hope I can find some ways to simplify it and streamline it.
I also ran into something funky when I was tracking mouse move events for editing the scenes within the scene's code. The problem was that changes were relative to the scene itself. So when I moved the scene in response to the move event, that would cause the next move event to register a delta in the opposite direction. The result was that the scenes would flicker back and forth as I edited them. I felt pretty silly once I realized the reason for that behavior. Once I tracked movements relative to the parent canvas, that problem went away.
I had initially wanted to use left click for editing time values and right click for drag and drop to reorder. However, WPF doesn't provide any means to do drag operations with the right click. So I had to do all drag operation with the left click. That meant all time editing actions had to be relegated to the right click.
I ended up going for a simple canvas as the holding element for better and worse.
Intended Upgrades
I hope to do several things to upgrade it, but I am not sure when I will get to it. If anybody does provide updates, that would be awesome, but I don't expect it. I am using this in my commercial and upgrades to this tool will find their way into it.
I hope to improve several things about it in the future:
- Snapping to discrete time units (though that may be tricky for me)
- Give it the ability to collapse empty gaps and drag/drop across them as if they weren't present. The current version doesn't really work well for a timeline that has large breaks in time.
- I hope to clean up and simplify the code. I am still somewhat of a newbie at building controls like this.
- I hope to improve its start up draw behavior so stretch horizontal alignment would be meaningful.
Acknowledgements
Users here on CodeProject were extremely helpful as I was developing this tool. As mentioned in the introduction, Steve Kirk's drag drop solution was also very useful. I wanted to post this as a way of thanking those who helped me get the control together. I welcome and look forward to feedback. I am sure it can be improved and cleaned up. I also learned a lot from people at the MSDN forums.
I hope to write a follow up article that details the implementation of the control sometime soon.
History
- Version 1 - August 2011
发表评论
ndGVNH you ave got an amazing blog right here! would you like to make some invite posts on my weblog?
ujHSSO You made some good points there. I looked on the net for additional information about the issue and found most individuals will go along with your views on this web site.
NeH1DY Im thankful for the blog.Thanks Again. Want more.
nvdlR8 It as hard to come by educated people on this subject, but you sound like you know what you are talking about! Thanks
slKACl Very good info. Lucky me I ran across your blog by accident (stumbleupon). I have saved as a favorite for later!
lI9vez You made some clear points there. I looked on the internet for the topic and found most guys will consent with your website.
FxLkuH There is visibly a lot to know about this. I feel you made various good points in features also.
aTj7VT Really appreciate you sharing this blog.Much thanks again. Great.
XbiDIu There is clearly a bunch to realize about this. I feel you made various good points in features also.
PMlDo8 I went over this site and I believe you have a lot of great info , saved to bookmarks (:.
UjujjM This blog is no doubt cool as well as factual. I have discovered helluva handy tips out of it. I ad love to visit it over and over again. Thanks a lot!
There as definately a great deal to learn about this subject. I really like all of the points you made.
A8jbxb Im thankful for the blog.Thanks Again. Really Great.
WH7Tzk Im grateful for the blog.Much thanks again. Want more.
zgegvG This website was how do you say it? Relevant!! Finally I have found something which helped me. Thanks a lot!
FlxdIp Say, you got a nice article post.Really thank you! Awesome.
XkSSkC
lwpgkq whoah this blog is magnificent i love reading your articles. Keep up the good work! You know, a lot of people are looking around for this information, you can aid them greatly.
lpkd52 Visit this I was suggested this web site by my cousin. I am not sure whether this post is written by him as nobody else know such detailed about my trouble. You are wonderful! Thanks!
qpSedC Im thankful for the article post.Really thank you! Great.
o9xeQU Thank you for sharing this very good post. Very interesting ideas! (as always, btw)
48JP7X Spot on with this write-up, I really assume this web site needs much more consideration. I'll probably be again to read way more, thanks for that info.
pX98K0 I like the helpful info you provide in your articles. I will bookmark your weblog and check again here frequently. I am quite certain I'll learn lots of new stuff right here! Good luck for the next!
Gc3DgG Heya! I'm at work browsing your blog from my new iphone 4! Just wanted to say I love reading through your blog and look forward to all your posts! Keep up the excellent work!
Ke568H Say, you got a nice article post.Thanks Again. Will read on...
dDW7VI Hey, thanks for the blog.Much thanks again. Really Cool.