Download : WP7JumpList2 (1)
Introduction
About a month ago, I created a Jump List control for Windows Phone 7 and published it on my blog. I got a lot of great feedback from the control, including questions about how certain parts of it work. As a result, I decided to publish an in-depth article here on CodeProject which describes the development of this control.
The control itself is quite dynamic, so the best way to get a feel for what it is like is to watch the following videos, one which is recorded from the emulator, the other on a real device – demonstrating the good performance of this control (apologies for the poor video quality!)
<OBJECT type=”application/x-shockwave-flash” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=3,0,0,0″ WIDTH=”480″ HEIGHT=”390″ data=”http://www.youtube.com/v/hdd1bIdSA-g?fs=1&hl=en_US”></OBJECT>
<OBJECT type=”application/x-shockwave-flash” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=3,0,0,0″ WIDTH=”480″ HEIGHT=”390″ data=”http://www.youtube.com/v/jk7jUeNNSHU?fs=1&hl=en_US”></OBJECT>
If you just want to grab the code and use the jump list in your application, then pop over to my blog where you will find a user guide and a number of examples. If you want to learn about how this control was put together, then read on …
Contents
Introduction
For Silverlight developers, Windows Phone 7 is a dream come true, a mobile platform that supports a language / framework they already know, or as Jesse Liberty puts it, “You are already a Windows Phone Developer“. What I find really cool about Silverlight for WP7 is that exactly the same controls can be used both on the web and the mobile. However, the controls for Windows Phone 7 are tailored specifically for the mobile form factor having larger areas to ‘hit’, and gestures for scrolling for example. Despite this, there are times when you really need a control that is specific to the mobile platform.
Navigating long lists of data is a chore on a mobile device. On the desktop / web, you can click on the scrollbar and navigate the full length of the list with a single gesture, whereas navigating the same list on a mobile requires multiple swipe gestures. This is where a Jump List comes in handy!
A Jump List groups the items within the long list into categories. Clicking on a category heading (or jump button) opens up a category view, where you can then click on one of the other categories, immediately causing the list to scroll to the start of this newly selected category.
This article describes the development of a Jump List control.
Developing the JumpList control
Creating a custom control
The first step when building a new control is to determine a suitable starting point, i.e., an existing framework class to extend. The jump list should support selection, so the framework Selector
class (which ListBox
subclasses) is a potential; however, it does not expose a public constructor, so that is a nonstarter! This just leaves Control
, so we’ll just have to start from there:
1 2 3 4 5 6 7 |
<span class="code-keyword">public</span> <span class="code-keyword">class</span> JumpList : Control { <span class="code-keyword">public</span> JumpList() { DefaultStyleKey = <span class="code-keyword">typeof</span>(JumpList); } } |
Control
, we are creating a ‘custom control’ (or as the Visual Studio ‘Add New Item’ dialog confusingly calls them, ‘Silverlight Templated Control’). The ‘look’ of the control, i.e., the various visual elements that are constructed to represent the control on screen, are defined as a Style
:
1 2 3 4 5 6 7 8 9 10 11 12 |
<span class="code-keyword"><</span><span class="code-leadattribute">Style</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">local:JumpList"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">Template"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ControlTemplate</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">local:JumpList"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Border</span> <span class="code-attribute">Background</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding Background}"</span> <span class="code-attribute">BorderBrush</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding BorderBrush}"</span> <span class="code-attribute">BorderThickness</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding BorderThickness}"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Border</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Style</span><span class="code-keyword">></span> |
DefaultStyleKey
of the JumpList
in the constructor to reference the style above ensures that this style is applied to any JumpList
instance that we create. The style sets a single property of the control, the Template
, to render a Border
. The various properties of the Border
are bound to various properties that we have inherited from Control
. The JumpList
control doesn’t really do much yet, although we can create an instance and set its various border properties:
1 2 3 |
<span class="code-keyword"><</span><span class="code-leadattribute">local:JumpList</span> <span class="code-attribute">Background</span><span class="code-keyword">="</span><span class="code-keyword">Pink"</span> <span class="code-attribute">BorderBrush</span><span class="code-keyword">="</span><span class="code-keyword">White"</span> <span class="code-attribute">BorderThickness</span><span class="code-keyword">="</span><span class="code-keyword">5"</span> <span class="code-attribute">Width</span><span class="code-keyword">="</span><span class="code-keyword">100"</span> <span class="code-attribute">Height</span><span class="code-keyword">="</span><span class="code-keyword">100"</span><span class="code-keyword">/</span><span class="code-keyword">></span> |

Rendering the items
The JumpList
needs to render a collection of items that the user supplies, where each item is rendered according to a template, mimicking the behavior of ListBox
(and other classes that render lists of objects, such as ComboBox
). To support this, we add an ItemsSource
dependency property of type IEnumerable
to the control. If you have created your own dependency properties before, you will know that there is quite a bit of boiler-plate code to deal with, which is why I prefer to use code-generation rather than add this code manually or via snippets. The technique I am using here is described in the blog post ‘declarative dependency property code generation‘, where you simply add an attribute to your class describing the property, and the code-generation adds the required code to a generated partial class.
Adding a dependency property is as simple as this …
1 2 3 4 5 6 7 8 9 |
[DependencyPropertyDecl(<span class="code-string">"</span><span class="code-string">ItemsSource"</span>, <span class="code-keyword">typeof</span>(IEnumerable), <span class="code-keyword">null</span>, <span class="code-string">"</span><span class="code-string">Gets or sets a collection used to generate the content of the JumpList"</span>)] <span class="code-keyword">public</span> <span class="code-keyword">partial</span> <span class="code-keyword">class</span> JumpList : Control { <span class="code-keyword">public</span> JumpList() { <span class="code-keyword">this</span>.DefaultStyleKey = <span class="code-keyword">typeof</span>(JumpList); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<span class="code-keyword">public</span> <span class="code-keyword">partial</span> <span class="code-keyword">class</span> JumpList { <span class="code-preprocessor">#region</span> ItemsSource <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-SummaryComment">///</span><span class="code-comment"> Gets or sets a collection used to generate the content </span> <span class="code-SummaryComment">///</span><span class="code-comment"> of the JumpList. This is a Dependency Property. </span> <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-keyword">public</span> IEnumerable ItemsSource { <span class="code-keyword">get</span> { <span class="code-keyword">return</span> (IEnumerable)GetValue(ItemsSourceProperty); } <span class="code-keyword">set</span> { SetValue(ItemsSourceProperty, value); } } <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-SummaryComment">///</span><span class="code-comment"> Identifies the ItemsSource Dependency Property. </span> <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-keyword">public</span> <span class="code-keyword">static</span> <span class="code-keyword">readonly</span> DependencyProperty ItemsSourceProperty = DependencyProperty.Register(<span class="code-string">"</span><span class="code-string">ItemsSource"</span>, <span class="code-keyword">typeof</span>(IEnumerable), <span class="code-keyword">typeof</span>(JumpList), <span class="code-keyword">new</span> PropertyMetadata(<span class="code-keyword">null</span>, OnItemsSourcePropertyChanged)); <span class="code-keyword">private</span> <span class="code-keyword">static</span> <span class="code-keyword">void</span> OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { JumpList myClass = d <span class="code-keyword">as</span> JumpList; myClass.OnItemsSourcePropertyChanged(e); } <span class="code-keyword">partial</span> <span class="code-keyword">void</span> OnItemsSourcePropertyChanged(DependencyPropertyChangedEventArgs e); <span class="code-preprocessor">#endregion</span> } |
ItemSource
property, we also need to expose a property that allows the user to specify how they want their items rendered. Following with the ItemsControl
naming conventions, we’ll add an ItemTemplate
property to the JumpList
:
1 2 3 4 5 6 7 8 9 10 11 |
[DependencyPropertyDecl(<span class="code-string">"</span><span class="code-string">ItemsSource"</span>, <span class="code-keyword">typeof</span>(IEnumerable), <span class="code-keyword">null</span>, <span class="code-string">"</span><span class="code-string">Gets or sets a collection used to generate the content of the JumpList"</span>)] [DependencyPropertyDecl(<span class="code-string">"</span><span class="code-string">ItemTemplate"</span>, <span class="code-keyword">typeof</span>(DataTemplate), <span class="code-keyword">null</span>, <span class="code-string">"</span><span class="code-string">Gets or sets the DataTemplate used to display each item"</span>)] <span class="code-keyword">public</span> <span class="code-keyword">partial</span> <span class="code-keyword">class</span> JumpList : Control { <span class="code-keyword">public</span> JumpList() { <span class="code-keyword">this</span>.DefaultStyleKey = <span class="code-keyword">typeof</span>(JumpList); } } |
In order to render the items that the user supplies to the ItemsSource
property (either by binding or by directly setting the property), we need to somehow add them to the visual tree of our JumpList when it is rendered. We could add them directly to the visual tree at runtime; however, the framework ItemsControl
provides a mechanism for rendering a bound collection of items within a panel, providing a simpler and more flexible solution. A collection of ContentControl
s are created in the code-behind, one for each of the bound items (later, this collection will also include group headings as well as the items themselves):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-SummaryComment">///</span><span class="code-comment"> Gets the categorised list of items </span><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-keyword">public</span> List<<span class="code-keyword">object</span>> FlattenedCategories { <span class="code-keyword">get</span> { <span class="code-keyword">return</span> _flattenedCategories; } <span class="code-keyword">private</span> <span class="code-keyword">set</span> { _flattenedCategories = value; OnPropertyChanged(<span class="code-string">"</span><span class="code-string">FlattenedCategories"</span>); } } <span class="code-keyword">private</span> <span class="code-keyword">void</span> RebuildCategorisedList() { <span class="code-keyword">if</span> (ItemsSource == <span class="code-keyword">null</span>) <span class="code-keyword">return</span>; <span class="code-keyword">var</span> jumpListItems = <span class="code-keyword">new</span> List<<span class="code-keyword">object</span>>(); <span class="code-keyword">foreach</span> (<span class="code-keyword">var</span> item <span class="code-keyword">in</span> ItemsSource) { jumpListItems.Add(<span class="code-keyword">new</span> ContentControl() { Content = item, ContentTemplate = ItemTemplate }); } FlattenedCategories = jumpListItems; } |
ItemsSource
property of the JumpList
is set, the above method, RebuildCategorisedList
, creates a list of ContentControl
s which the JumpList
exposes via the FlattenedCategories
property. All we have to do now to add them to the visual tree of our JumpList
is add an ItemsControl
to the template, binding it to the FlattenedCategories
property via a RelativeSource
–TemplatedParent
binding.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<span class="code-keyword"><</span><span class="code-leadattribute">Style</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">local:JumpList"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">Template"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ControlTemplate</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">local:JumpList"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Border</span> <span class="code-attribute">Background</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding Background}"</span> <span class="code-attribute">BorderBrush</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding BorderBrush}"</span> <span class="code-attribute">BorderThickness</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding BorderThickness}"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsControl</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">JumpListItems"</span> <span class="code-attribute">ItemsSource</span><span class="code-keyword">="</span><span class="code-keyword">{Binding RelativeSource={RelativeSource TemplatedParent},Path=FlattenedCategories}"</span><span class="code-keyword">></span> <span class="code-comment"><!--</span><span class="code-keyword"><span class="code-comment"> use a virtualizing stack panel to host our items </span>--></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsControl.ItemsPanel</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsPanelTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">VirtualizingStackPanel</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsPanelTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsControl.ItemsPanel</span><span class="code-keyword">></span> <span class="code-comment"><!--</span><span class="code-keyword"><span class="code-comment"> template, which adds a scroll viewer </span>--></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsControl.Template</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ControlTemplate</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">ItemsControl"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ScrollViewer</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">ScrollViewer"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsPresenter</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ScrollViewer</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsControl.Template</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsControl</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Border</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Style</span><span class="code-keyword">></span> |
JumpList
is instantiated with the following template, where the ItemsSource
is a collection of Person
objects (with properties of Surname
and Forename
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<span class="code-keyword"><</span><span class="code-leadattribute">local:JumpList</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">local:JumpList.ItemTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DataTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">StackPanel</span> <span class="code-attribute">Orientation</span><span class="code-keyword">="</span><span class="code-keyword">Horizontal"</span> <span class="code-attribute">Margin</span><span class="code-keyword">="</span><span class="code-keyword">0,3,0,3"</span> <span class="code-attribute">Height</span><span class="code-keyword">="</span><span class="code-keyword">40"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">TextBlock</span> <span class="code-attribute">Text</span><span class="code-keyword">="</span><span class="code-keyword">{Binding Surname}"</span> <span class="code-attribute">Margin</span><span class="code-keyword">="</span><span class="code-keyword">3,0,0,0"</span> <span class="code-attribute">VerticalAlignment</span><span class="code-keyword">="</span><span class="code-keyword">Center"</span> <span class="code-attribute">FontSize</span><span class="code-keyword">="</span><span class="code-keyword">{StaticResource PhoneFontSizeLarge}"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">TextBlock</span> <span class="code-attribute">Text</span><span class="code-keyword">="</span><span class="code-keyword">, "</span> <span class="code-attribute">VerticalAlignment</span><span class="code-keyword">="</span><span class="code-keyword">Center"</span> <span class="code-attribute">FontSize</span><span class="code-keyword">="</span><span class="code-keyword">{StaticResource PhoneFontSizeLarge}"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">TextBlock</span> <span class="code-attribute">Text</span><span class="code-keyword">="</span><span class="code-keyword">{Binding Forename}"</span> <span class="code-attribute">VerticalAlignment</span><span class="code-keyword">="</span><span class="code-keyword">Center"</span> <span class="code-attribute">FontSize</span><span class="code-keyword">="</span><span class="code-keyword">{StaticResource PhoneFontSizeLarge}"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">StackPanel</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">DataTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">local:JumpList.ItemTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">local:JumpList</span><span class="code-keyword">></span> |
JumpList
would render as follows:
Handling CollectionChanged events
The ItemsSource
property is of type IEnumerable
, which is the only requirement we have on the supplied data in order to render it. This gives the user great flexibility; they can supply a List
, Array
, or assign the ItemsSource
directly to the result of a LINQ query. However, they may also set (or bind) this property to an ObservableCollection
, with the expectation that the JumpList
is updated when they add or remove items from the list. In order to support this requirement, we need to ‘probe’ the ItemsSource
to see if it implements INotifyCollectionChanged
(the interface that makes ObservableCollection
work), and update our list accordingly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<span class="code-comment">//</span><span class="code-comment"> invoked when the ItemsSource dependency property changes </span><span class="code-keyword">partial</span> <span class="code-keyword">void</span> OnItemsSourcePropertyChanged(DependencyPropertyChangedEventArgs e) { INotifyCollectionChanged oldIncc = e.OldValue <span class="code-keyword">as</span> INotifyCollectionChanged; <span class="code-keyword">if</span> (oldIncc != <span class="code-keyword">null</span>) { oldIncc.CollectionChanged -= ItemsSource_CollectionChanged; } INotifyCollectionChanged incc = e.NewValue <span class="code-keyword">as</span> INotifyCollectionChanged; <span class="code-keyword">if</span> (incc != <span class="code-keyword">null</span>) { incc.CollectionChanged += ItemsSource_CollectionChanged; } RebuildCategorisedList(); } <span class="code-comment">//</span><span class="code-comment"> handles collection changed events, rebuilding the list </span><span class="code-keyword">private</span> <span class="code-keyword">void</span> ItemsSource_CollectionChanged(<span class="code-keyword">object</span> sender, NotifyCollectionChangedEventArgs e) { RebuildCategorisedList(); } |
NotifyCollectionChangedEventArgs.Action
parameter, modifying our exposed list, rather than completely rebuilding it where appropriate.
Adding categories
So far the control simply renders the list of items, doing nothing more than an ItemsControl
would. In order to make this into a jump list, we need to assign items to categories. In order to provide flexibility regarding how items are assigned to categories, we give the user of the control this responsibility via the ICategoryProvider
interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-SummaryComment">///</span><span class="code-comment"> A category provider assigns items to categories and details </span><span class="code-SummaryComment">///</span><span class="code-comment"> the full category list for a set of items. </span><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-keyword">public</span> <span class="code-keyword">interface</span> ICategoryProvider { <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-SummaryComment">///</span><span class="code-comment"> Gets the category for the given items </span> <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-keyword">object</span> GetCategoryForItem(<span class="code-keyword">object</span> item); <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-SummaryComment">///</span><span class="code-comment"> Gets the full list of categories for the given items. </span> <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> List<<span class="code-keyword">object</span>> GetCategoryList(IEnumerable items); } |
1 2 3 4 5 6 7 8 |
... [DependencyPropertyDecl(<span class="code-string">"</span><span class="code-string">CategoryProvider"</span>, <span class="code-keyword">typeof</span>(ICategoryProvider), <span class="code-keyword">null</span>, <span class="code-string">"</span><span class="code-string">Gets or sets a category provider which groups the items "</span> + <span class="code-string">"</span><span class="code-string">in the JumpList and specifies the categories in the jump menu"</span>)] <span class="code-keyword">public</span> <span class="code-keyword">partial</span> <span class="code-keyword">class</span> JumpList : Control, INotifyPropertyChanged { ... } |
PropertyName
. The category list is the complete alphabet, in order:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-SummaryComment">///</span><span class="code-comment"> A category provider that categorizes items </span><span class="code-SummaryComment">///</span><span class="code-comment"> based on the first character of the </span><span class="code-SummaryComment">///</span><span class="code-comment"> property named via the PropertyName property. </span><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-keyword">public</span> <span class="code-keyword">class</span> AlphabetCategoryProvider : ICategoryProvider { <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-SummaryComment">///</span><span class="code-comment"> Gets or sets the name of the property that is used to assign each item </span> <span class="code-SummaryComment">///</span><span class="code-comment"> to a category. </span> <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span> <span class="code-keyword">public</span> <span class="code-keyword">string</span> PropertyName { <span class="code-keyword">get</span>; <span class="code-keyword">set</span>;} <span class="code-keyword">public</span> <span class="code-keyword">object</span> GetCategoryForItem(<span class="code-keyword">object</span> item) { <span class="code-keyword">var</span> propInfo = item.GetType().GetProperty(PropertyName); <span class="code-keyword">object</span> propertyValue = propInfo.GetValue(item, <span class="code-keyword">null</span>); <span class="code-keyword">return</span> ((<span class="code-keyword">string</span>)propertyValue).Substring(<span class="code-digit">0</span>, <span class="code-digit">1</span>).ToUpper(); } <span class="code-keyword">public</span> List<<span class="code-keyword">object</span>> GetCategoryList(IEnumerable items) { <span class="code-keyword">return</span> Enumerable.Range(<span class="code-digit">0</span>, <span class="code-digit">26</span>) .Select(index => Convert.ToChar( (Convert.ToInt32(<span class="code-string">'</span><span class="code-string">A'</span>) + index)).ToString()) .Cast<<span class="code-keyword">object</span>>() .ToList(); } } |
JumpList
. The user of the control can simply set the CategoryProvider
to an instance of the provider above. For example, if the control is being used to render Person
objects (which have properties of Surname
and Forename
), the XAML for the JumpList
would be as follows:
1 2 3 4 5 |
<span class="code-keyword"><</span><span class="code-leadattribute">local:JumpList</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">local:JumpList.CategoryProvider</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">local:AlphabetCategoryProvider</span> <span class="code-attribute">PropertyName</span><span class="code-keyword">="</span><span class="code-keyword">Surname"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">local:JumpList.CategoryProvider</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">local:JumpList</span><span class="code-keyword">></span> |
RebuildCategorisedList
method described above which creates a list of ContentControl
s, one for each item in the list, can now be updated to add the category headings (i.e., the jump-buttons). We want the user of the JumpList
to be able to style these jump buttons, so some further dependency properties are added:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... [DependencyPropertyDecl(<span class="code-string">"</span><span class="code-string">JumpButtonItemTemplate"</span>, <span class="code-keyword">typeof</span>(DataTemplate), <span class="code-keyword">null</span>, <span class="code-string">"</span><span class="code-string">Gets or sets the DataTemplate used to display the Jump buttons. "</span> + <span class="code-string">"</span><span class="code-string">The DataContext of each button is a group key"</span>)] DependencyPropertyDecl(<span class="code-string">"</span><span class="code-string">JumpButtonTemplate"</span>, <span class="code-keyword">typeof</span>(ControlTemplate), <span class="code-keyword">null</span>, <span class="code-string">"</span><span class="code-string">Gets or sets the ControlTemplate for the Jump buttons"</span>)] [DependencyPropertyDecl(<span class="code-string">"</span><span class="code-string">JumpButtonStyle"</span>, <span class="code-keyword">typeof</span>(Style), <span class="code-keyword">null</span>, <span class="code-string">"</span><span class="code-string">Gets or sets the style applied to the Jump buttons. "</span> + <span class="code-string">"</span><span class="code-string">This should be a style with a TargetType of Button"</span>)] <span class="code-keyword">public</span> <span class="code-keyword">class</span> JumpList : Control { ... } |
JumpButtonStyle
; if they want to change the template, or add an icon for example, they can set the JumpButtonTemplate
; finally, they can specify how the ‘object’ that represents each item’s category is rendered via the JumpButtonItemTemplate
, this allows them to format a date, for example.
The RebuildCategorisedList
is expanded to group the items based on the category provider via a simple LINQ query. Buttons are added to the collection of objects exposed to the ItemsControl
within the JumpList
template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<span class="code-keyword">private</span> <span class="code-keyword">void</span> RebuildCategorisedList() { <span class="code-keyword">if</span> (ItemsSource == <span class="code-keyword">null</span>) <span class="code-keyword">return</span>; <span class="code-comment">//</span><span class="code-comment"> adds each item into a category </span> <span class="code-keyword">var</span> categorisedItemsSource = ItemsSource.Cast<<span class="code-keyword">object</span>>() .GroupBy(i => CategoryProvider.GetCategoryForItem(i)) .OrderBy(g => g.Key) .ToList(); <span class="code-comment">//</span><span class="code-comment"> create the jump list </span> <span class="code-keyword">var</span> jumpListItems = <span class="code-keyword">new</span> List<<span class="code-keyword">object</span>>(); <span class="code-keyword">foreach</span> (<span class="code-keyword">var</span> category <span class="code-keyword">in</span> categorisedItemsSource) { jumpListItems.Add(<span class="code-keyword">new</span> Button() { Content = category.Key, ContentTemplate = JumpButtonItemTemplate, Template = JumpButtonTemplate, Style = JumpButtonStyle }); jumpListItems.AddRange(category.Select(item => <span class="code-keyword">new</span> ContentControl() { Content = item, ContentTemplate = ItemTemplate }).Cast<<span class="code-keyword">object</span>>()); } <span class="code-comment">//</span><span class="code-comment"> add interaction handlers </span> <span class="code-keyword">foreach</span> (<span class="code-keyword">var</span> button <span class="code-keyword">in</span> jumpListItems.OfType<Button>()) { button.Click += JumpButton_Click; } } <span class="code-keyword">private</span> <span class="code-keyword">void</span> JumpButton_Click(<span class="code-keyword">object</span> sender, RoutedEventArgs e) { IsCategoryViewShown = <span class="code-keyword">true</span>; } |
Button.Click
event handler is added to each of the buttons that are created – more on this later!
We can set the default values for the three jump-button properties by adding property setters to the JumpList
default style (in the generic.xaml file):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<span class="code-keyword"><</span><span class="code-leadattribute">Style</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">l:JumpList"</span><span class="code-keyword">></span> <span class="code-comment"><!--</span><span class="code-keyword"><span class="code-comment"> style the buttons to be left aligned with some padding </span>--></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">JumpButtonStyle"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Style</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">Button"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">HorizontalAlignment"</span> <span class="code-attribute">Value</span><span class="code-keyword">="</span><span class="code-keyword">Left"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">HorizontalContentAlignment"</span> <span class="code-attribute">Value</span><span class="code-keyword">="</span><span class="code-keyword">Stretch"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">VerticalContentAlignment"</span> <span class="code-attribute">Value</span><span class="code-keyword">="</span><span class="code-keyword">Stretch"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">Padding"</span> <span class="code-attribute">Value</span><span class="code-keyword">="</span><span class="code-keyword">8"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Style</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter</span><span class="code-keyword">></span> <span class="code-comment"><!--</span><span class="code-keyword"><span class="code-comment"> an item template that simply displays the category 'object' </span>--></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">JumpButtonItemTemplate"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DataTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">TextBlock</span> <span class="code-attribute">Text</span><span class="code-keyword">="</span><span class="code-keyword">{Binding}"</span> <span class="code-attribute">FontSize</span><span class="code-keyword">="</span><span class="code-keyword">{StaticResource PhoneFontSizeMedium}"</span> <span class="code-attribute">Padding</span><span class="code-keyword">="</span><span class="code-keyword">5"</span> <span class="code-attribute">VerticalAlignment</span><span class="code-keyword">="</span><span class="code-keyword">Bottom"</span> <span class="code-attribute">HorizontalAlignment</span><span class="code-keyword">="</span><span class="code-keyword">Left"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">DataTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter</span><span class="code-keyword">></span> <span class="code-comment"><!--</span><span class="code-keyword"><span class="code-comment"> the template for our button, a simplified version of the standard button </span>--></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">JumpButtonTemplate"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Grid</span> <span class="code-attribute">Background</span><span class="code-keyword">="</span><span class="code-keyword">Transparent"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">VisualStateManager.VisualStateGroups</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">VisualStateGroup</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">CommonStates"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">VisualState</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">Normal"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">VisualState</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">MouseOver"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">VisualState</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">Pressed"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Storyboard</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ColorAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">White"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">Background"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">= "</span><span class="code-keyword">(Rectangle.Fill).(SolidColorBrush.Color)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Storyboard</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">VisualState</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">VisualState</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">Disabled"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">VisualStateGroup</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">VisualStateManager.VisualStateGroups</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Rectangle</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">Background"</span> <span class="code-attribute">Fill</span><span class="code-keyword">="</span><span class="code-keyword">{StaticResource PhoneAccentBrush}"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ContentControl</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">ContentContainer"</span> <span class="code-attribute">Foreground</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding Foreground}"</span> <span class="code-attribute">HorizontalContentAlignment</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding HorizontalContentAlignment}"</span> <span class="code-attribute">VerticalContentAlignment</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding VerticalContentAlignment}"</span> <span class="code-attribute">Padding</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding Padding}"</span> <span class="code-attribute">Content</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding Content}"</span> <span class="code-attribute">ContentTemplate</span><span class="code-keyword">="</span><span class="code-keyword">{TemplateBinding ContentTemplate}"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Grid</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter</span><span class="code-keyword">></span> ... <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Style</span><span class="code-keyword">></span> |
JumpButtonStyle
and JumpButtonItemTemplate
property values are quite simple. The JumpButtonTemplate
is a little more complex; here we are defining the template used to render our buttons. Rather than using the default button template, which is black with a white border, the jump buttons are templated to be a solid rectangle filled with the phone’s accent colour (a user-specified colour which is used for live tiles etc…). The VisualStateManager
markup has a single VisualState
defined which makes the button turn white when it is pressed.
The control is now starting to look like a jump list …
The Category view
When a user clicks on a jump button, we want to display a menu which allows them to jump to a specific category. In order to achieve this, we need to create another ‘view’ of our data which is hidden, revealing it when a button is clicked.
We can expand the method which builds our categorized list of items and jump buttons to expose a list of categories:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<span class="code-keyword">private</span> <span class="code-keyword">void</span> RebuildCategorisedList() { <span class="code-comment">//</span><span class="code-comment"> adds each item into a category </span> <span class="code-keyword">var</span> categorisedItemsSource = ItemsSource.Cast<<span class="code-keyword">object</span>>() .GroupBy(i => CategoryProvider.GetCategoryForItem(i)) .OrderBy(g => g.Key) .ToList(); <span class="code-comment">//</span><span class="code-comment"> ... jump list creation code as per above ... </span> <span class="code-comment">//</span><span class="code-comment"> creates the category view, where the active state is determined by whether </span> <span class="code-comment">//</span><span class="code-comment"> there are any items in the category </span> CategoryList = CategoryProvider.GetCategoryList(ItemsSource) .Select(category => <span class="code-keyword">new</span> Button() { Content = category, IsEnabled = categorisedItemsSource.Any( categoryItems => categoryItems.Key.Equals(category)), ContentTemplate = <span class="code-keyword">this</span>.CategoryButtonItemTemplate, Style = <span class="code-keyword">this</span>.CategoryButtonStyle, Template = <span class="code-keyword">this</span>.CategoryButtonTemplate }).Cast<<span class="code-keyword">object</span>>().ToList(); <span class="code-keyword">foreach</span> (<span class="code-keyword">var</span> button <span class="code-keyword">in</span> CategoryList.OfType<Button>()) { button.Click += CategoryButton_Click; } } |
The following markup is added to the template, binding an items control to a CategoryList
property, using the same technique as the items control which renders the jump list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="code-keyword"><</span><span class="code-leadattribute">ItemsControl</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">CategoryItems"</span> <span class="code-attribute">Visibility</span><span class="code-keyword">="</span><span class="code-keyword">Collapsed"</span> <span class="code-attribute">ItemsSource</span><span class="code-keyword">="</span><span class="code-keyword">{Binding RelativeSource= {RelativeSource TemplatedParent}, Path=CategoryList}"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsControl.ItemsPanel</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsPanelTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">tk:WrapPanel</span> <span class="code-attribute">Background</span><span class="code-keyword">="</span><span class="code-keyword">Transparent"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsPanelTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsControl.ItemsPanel</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsControl.Template</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ScrollViewer</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">ScrollViewer"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsPresenter</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ScrollViewer</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsControl.Template</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsControl</span><span class="code-keyword">></span> |
WrapPanel
, which gives the following result:
Switching views
The control now has two different ‘views’, one which is the categorized list of items together with jump-buttons, and the other, the category view. All we have to do is handle the Click
event on the jump-buttons to show the category view. For greater flexibility, this behavior is exposed via a bool IsCategoryViewShown
property. When a jump-button is clicked, this property is set to true
, and the change handler for the property takes care of switching the view. This provides greater flexibility to the user of the control, allowing them to switch views programmatically.
In order to show / hide the category views and list views that are defined in the JumpList
template, we need to obtain references to them. With UserControl
, elements named with the x:Name
attribute are automatically wired-up to fields in the corresponding code-behind class. However, with custom controls, you have to do this wire-up yourself. The following code locates the ItemsControl
s for the jump list and category view:
1 2 3 4 5 6 7 8 |
<span class="code-keyword">private</span> ItemsControl _jumpListControl; <span class="code-keyword">private</span> ItemsControl _categoryItemsControl; <span class="code-keyword">public</span> <span class="code-keyword">override</span> <span class="code-keyword">void</span> OnApplyTemplate() { <span class="code-keyword">base</span>.OnApplyTemplate(); _jumpListControl = <span class="code-keyword">this</span>.GetTemplateChild(<span class="code-string">"</span><span class="code-string">JumpListItems"</span>) <span class="code-keyword">as</span> ItemsControl; _categoryItemsControl = <span class="code-keyword">this</span>.GetTemplateChild(<span class="code-string">"</span><span class="code-string">CategoryItems"</span>) <span class="code-keyword">as</span> ItemsControl; } |
GetTemplateChild
matches the x:Name
for each of these elements.
The code generated for each dependency property adds a call to a partial method which is invoked when the property changes. This allows you to add logic that is executed as a result of the property change. The following method is invoked each time the IsCategoryViewShown
property is changed, it simply shows / hides the items control:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="code-keyword">partial</span> <span class="code-keyword">void</span> OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e) { <span class="code-keyword">if</span> ((<span class="code-keyword">bool</span>)e.NewValue == <span class="code-keyword">true</span>) { _jumpListControl.Visibility = Visibility.Collapsed; _categoryItemsControl.Visibility = Visibility.Visible; } <span class="code-keyword">else</span> { _jumpListControl.Visibility = Visibility.Visible; _categoryItemsControl.Visibility = Visibility.Collapsed; } } |
Making the jump
We have already seen that the method RebuildCategorisedList
adds a Click
event handler to the category buttons. We now need to add the code which makes the list ‘jump’ to the required location. The ItemsControl
which renders our list of categorized items uses a VirtualizingStackPanel
as the container for the items, and places this within a ScrollViewer
. The VirtualizingStackPanel
has a method SetVerticalOffset
which can be used to scroll it to a specific index, allowing us to make the list jump.
The first thing we need to do is locate the VirtualizingStackPanel
. Unlike the other named elements in our template, this element cannot be retrieved by GetTemplateChild
within OnApplyTemplate
because it is in a different XAML namespace (also, it may not be created initially, if the ItemsControl
does not have any items to render). In order to locate the VirtualizingStackPanel
when we need it, we can use LINQ-to-VisualTree to query the descendant elements of our ItemsControl
to locate the element of the required type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-SummaryComment">///</span><span class="code-comment"> Gets the stack panel that hosts our jump list items </span><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-keyword">private</span> VirtualizingStackPanel ItemsHostStackPanel { <span class="code-keyword">get</span> { <span class="code-keyword">if</span> (_stackPanel == <span class="code-keyword">null</span>) { _stackPanel = _jumpListControl.Descendants<VirtualizingStackPanel>() .Cast<VirtualizingStackPanel>() .SingleOrDefault(); } <span class="code-keyword">return</span> _stackPanel; } } |
Content
, i.e., the category returned by the ICategoryProvider
). Once the corresponding button is found, we can find its index, offset the VirtualizingStackPanel
, then switch back to the jump-list view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="code-keyword">private</span> <span class="code-keyword">void</span> CategoryButton_Click(<span class="code-keyword">object</span> sender, RoutedEventArgs e) { <span class="code-keyword">var</span> categoryButton = sender <span class="code-keyword">as</span> Button; <span class="code-comment">//</span><span class="code-comment"> find the jump button for this category </span> <span class="code-keyword">var</span> button = FlattenedCategories.OfType<Button>() .Where(b => b.Content.Equals(categoryButton.Content)) .SingleOrDefault(); <span class="code-comment">//</span><span class="code-comment"> button is null if there are no items in the clicked category </span> <span class="code-keyword">if</span> (button != <span class="code-keyword">null</span>) { <span class="code-comment">//</span><span class="code-comment"> find the button index </span> <span class="code-keyword">var</span> index = FlattenedCategories.IndexOf(button); ItemsHostStackPanel.SetVerticalOffset(index); IsCategoryViewShown = <span class="code-keyword">false</span>; } } |
JumpList
control!
Jazzing it up!
The control we have developed so far works well; however, it is lacking in flare (we don’t want our iPhone and Android friends to think theirs is a better platform, do we?). We could spice up the graphics, adding drop shadows, gradients, images etc… however, that is not really in keeping with the Windows Phone 7 Metro theme which favours clear typography and sparse graphics coupled with fluid animations. In this section, we will look at how to make this control more visually appealing via animations, whilst maintaining its clean and simple style.
Simple Show / Hide animations
When the jump list control switches between the category and list views, the following code simply shows / hides the respective items controls for each of these views:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="code-keyword">partial</span> <span class="code-keyword">void</span> OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e) { <span class="code-keyword">if</span> ((<span class="code-keyword">bool</span>)e.NewValue == <span class="code-keyword">true</span>) { _jumpListControl.Visibility = Visibility.Collapsed; _categoryItemsControl.Visibility = Visibility.Visible; } <span class="code-keyword">else</span> { _jumpListControl.Visibility = Visibility.Visible; _categoryItemsControl.Visibility = Visibility.Collapsed; } } |
FrameworkElement
extension methods, Show()
and Hide()
, which inspect the element resources to find a storyboard which can be used to show or hide the element. If no storyboard is present, the Visibility
property is set instead. Applying this method, the above code becomes:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="code-keyword">partial</span> <span class="code-keyword">void</span> OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e) { <span class="code-keyword">if</span> ((<span class="code-keyword">bool</span>)e.NewValue == <span class="code-keyword">true</span>) { _jumpListControl.Hide(); _categoryItemsControl.Show(); } <span class="code-keyword">else</span> { _jumpListControl.Show(); _categoryItemsControl.Hide(); } } |
ItemsControl
which renders the jump list, to include storyboards which alter the control’s opacity in order to provide a fade-in / fade-out effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<span class="code-keyword"><</span><span class="code-leadattribute">l:JumpListItemsControl</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">JumpListItems"</span> <span class="code-attribute">ItemsSource</span><span class="code-keyword">="</span><span class="code-keyword">{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FlattenedCategories}"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">l:JumpListItemsControl.Resources</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Storyboard</span> <span class="code-attribute">x:Key</span><span class="code-keyword">="</span><span class="code-keyword">JumpListItemsShowAnim"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DoubleAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">1.0"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0.5"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">JumpListItems"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">="</span><span class="code-keyword">(ScrollViewer.Opacity)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Storyboard</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Storyboard</span> <span class="code-attribute">x:Key</span><span class="code-keyword">="</span><span class="code-keyword">JumpListItemsHideAnim"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DoubleAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">0.35"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0.5"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">JumpListItems"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">="</span><span class="code-keyword">(ScrollViewer.Opacity)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Storyboard</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">l:JumpListItemsControl.Resources</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">l:JumpListItemsControl.ItemsPanel</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsPanelTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">VirtualizingStackPanel</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ItemsPanelTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">l:JumpListItemsControl.ItemsPanel</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">l:JumpListItemsControl.Template</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ControlTemplate</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">l:JumpListItemsControl"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ScrollViewer</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">ScrollViewer"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ItemsPresenter</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ScrollViewer</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">l:JumpListItemsControl.Template</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">l:JumpListItemsControl</span><span class="code-keyword">></span> |
Show()
/ Hide()
extension methods, please refer to my earlier blog post.
Adding a Loading indicator
If you have only developed Windows Phone 7 applications using the emulator, you will probably have some false impressions regarding your application’s responsiveness. The real Windows Phone 7 hardware is typically much less powerful than the emulated hardware on your whizzy developer machine! I made a few measurements and found that my developer machine emulator rendered each page approximately four times faster than a real device. However, results will of course vary from one machine to the next. The real take-home message here is, “test on the real hardware”. This section describes a few simple changes to the jump list which should ensure a rapid initial render time.
If the category view ItemsControl
is shown by animating its opacity from 0 to 1.0, then all the visual elements of the category view will be rendered when the jump list is first displayed, even though the user cannot see the category view. This can add as much as half a second to the overall load time of the control. If the visibility of the ItemsControl
is initially set to Collapsed
(in generic.xaml), then the overhead of the numerous child elements it contains will be removed. However, this still does not remove the half-second additional render time for the category view, it just postpones it until later on. We do not want the jump list to simply ‘stall’ the first time a button is clicked, therefore a small loading indicator is added so that the user knows that the phone is doing something …
When a category button is clicked, we initially show a simple loading message, then set the category view visibility to Visible
, causing the expensive initial construction of this view. When the category view and its child elements are created, a LayoutUpdated
event will fire; we can handle this event in order to hide the loading indicator.
This simple loading indicator is added to the jump list template:
1 2 3 4 5 6 |
<span class="code-keyword"><</span><span class="code-leadattribute">Grid</span> <span class="code-attribute">IsHitTestVisible</span><span class="code-keyword">="</span><span class="code-keyword">False"</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">LoadingIndicator"</span> <span class="code-attribute">Opacity</span><span class="code-keyword">="</span><span class="code-keyword">0"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">TextBlock</span> <span class="code-attribute">Text</span><span class="code-keyword">="</span><span class="code-keyword">Loading ..."</span> <span class="code-attribute">HorizontalAlignment</span><span class="code-keyword">="</span><span class="code-keyword">Right"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Grid</span><span class="code-keyword">></span> |
IsCategoryViewShown
property changed is updated to show this loading indicator the first time the category view is shown. The next time it is shown, we do not need the loading indicator because the category view UI is already built and has just been hidden by setting its opacity to zero.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<span class="code-keyword">partial</span> <span class="code-keyword">void</span> OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e) { <span class="code-keyword">if</span> ((<span class="code-keyword">bool</span>)e.NewValue == <span class="code-keyword">true</span>) { _jumpListControl.Hide(); <span class="code-comment">//</span><span class="code-comment"> first time load! </span> <span class="code-keyword">if</span> (_categoryItemsControl.Visibility == Visibility.Collapsed) { <span class="code-comment">//</span><span class="code-comment"> show the loading indicator </span> _loadingIndicator.Opacity = <span class="code-digit">1</span>; Dispatcher.BeginInvoke(() => { <span class="code-comment">//</span><span class="code-comment"> handle layout updated </span> _categoryItemsControl.LayoutUpdated += <span class="code-keyword">new</span> EventHandler(CategoryItemsControl_LayoutUpdated); <span class="code-comment">//</span><span class="code-comment"> make the items control visible so that its UI is built </span> _categoryItemsControl.Visibility = Visibility.Visible; }); } <span class="code-keyword">else</span> { _jumpListControl.IsHitTestVisible = <span class="code-keyword">false</span>; _categoryItemsControl.IsHitTestVisible = <span class="code-keyword">true</span>; _categoryItemsControl.Show(); } } <span class="code-keyword">else</span> { _jumpListControl.Show(); _jumpListControl.IsHitTestVisible = <span class="code-keyword">true</span>; _categoryItemsControl.IsHitTestVisible = <span class="code-keyword">false</span>; _categoryItemsControl.Hide(); } } <span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-SummaryComment">///</span><span class="code-comment"> Handles LayoutUpdated event in order to hide the loading indicator </span><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-keyword">private</span> <span class="code-keyword">void</span> CategoryItemsControl_LayoutUpdated(<span class="code-keyword">object</span> sender, EventArgs e) { _categoryItemsControl.LayoutUpdated -= CategoryItemsControl_LayoutUpdated; _loadingIndicator.Visibility = System.Windows.Visibility.Collapsed; Dispatcher.BeginInvoke(() => { <span class="code-comment">//</span><span class="code-comment"> play the 'show' animation </span> _categoryItemsControl.Show(); }); } |
DeferredLoadContentControl
which initially displays a ‘loading…’ message whilst the more complex content is constructed. You can read about this control on my blog.
Animating the ‘jump’
The jump list control described so far sets the vertical offset of our list directly so that the jump button which heads each category is immediately brought into view. In this section, we will look at how to animate this, so that the selected category scrolls smoothly into view.
The vertical offset of our list is changed via the following code:
1 |
ItemsHostStackPanel.SetVerticalOffset(index); |
Here’s the private dependency property, with the callback that sets the vertical offset:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-SummaryComment">///</span><span class="code-comment"> VerticalOffset, a private DP used to animate the scrollviewer </span><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment"><</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">></span> </span><span class="code-keyword">private</span> DependencyProperty VerticalOffsetProperty = DependencyProperty.Register(<span class="code-string">"</span><span class="code-string">VerticalOffset"</span>, <span class="code-keyword">typeof</span>(<span class="code-keyword">double</span>), <span class="code-keyword">typeof</span>(JumpList), <span class="code-keyword">new</span> PropertyMetadata(<span class="code-digit">0</span>.<span class="code-digit">0</span>, OnVerticalOffsetChanged)); <span class="code-keyword">private</span> <span class="code-keyword">static</span> <span class="code-keyword">void</span> OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { JumpList jumpList = d <span class="code-keyword">as</span> JumpList; jumpList.OnVerticalOffsetChanged(e); } <span class="code-keyword">private</span> <span class="code-keyword">void</span> OnVerticalOffsetChanged(DependencyPropertyChangedEventArgs e) { ItemsHostStackPanel.SetVerticalOffset((<span class="code-keyword">double</span>)e.NewValue); } |
DoubleAnimation
which uses a Sine easing function, which accelerates at the start and decelerates at the end (providing a smoother experience), is created:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<span class="code-keyword">public</span> JumpList() { DefaultStyleKey = <span class="code-keyword">typeof</span>(JumpList); RebuildCategorisedList(); <span class="code-comment">//</span><span class="code-comment"> create a scroll animation </span> _scrollAnimation = <span class="code-keyword">new</span> DoubleAnimation(); _scrollAnimation.EasingFunction = <span class="code-keyword">new</span> SineEase(); <span class="code-comment">//</span><span class="code-comment"> create a storyboard for the animation </span> _scrollStoryboard = <span class="code-keyword">new</span> Storyboard(); _scrollStoryboard.Children.Add(_scrollAnimation); Storyboard.SetTarget(_scrollAnimation, <span class="code-keyword">this</span>); Storyboard.SetTargetProperty(_scrollAnimation, <span class="code-keyword">new</span> PropertyPath(<span class="code-string">"</span><span class="code-string">VerticalOffset"</span>)); <span class="code-comment">//</span><span class="code-comment"> Make the Storyboard a resource. </span> Resources.Add(<span class="code-string">"</span><span class="code-string">anim"</span>, _scrollStoryboard); } |
ScrollDuration
dependency property to the JumpList
control. All that is left to do is use the above animation when the category button is clicked:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<span class="code-keyword">private</span> <span class="code-keyword">void</span> CategoryButton_Click(<span class="code-keyword">object</span> sender, RoutedEventArgs e) { <span class="code-keyword">var</span> categoryButton = sender <span class="code-keyword">as</span> Button; <span class="code-comment">//</span><span class="code-comment"> find the jump button for this category </span> <span class="code-keyword">var</span> button = FlattenedCategories.OfType<Button>() .Where(b => b.Content.Equals(categoryButton.Content)) .SingleOrDefault(); <span class="code-comment">//</span><span class="code-comment"> button is null if there are no items in the clicked category </span> <span class="code-keyword">if</span> (button != <span class="code-keyword">null</span>) { <span class="code-comment">//</span><span class="code-comment"> find the button index </span> <span class="code-keyword">var</span> index = FlattenedCategories.IndexOf(button); <span class="code-keyword">if</span> (ScrollDuration > <span class="code-digit">0</span>.<span class="code-digit">0</span>) { _scrollAnimation.Duration = TimeSpan.FromMilliseconds(ScrollDuration); _scrollStoryboard.Duration = TimeSpan.FromMilliseconds(ScrollDuration); _scrollAnimation.To = (<span class="code-keyword">double</span>)index; _scrollAnimation.From = ItemsHostStackPanel.ScrollOwner.VerticalOffset; _scrollStoryboard.Begin(); } <span class="code-keyword">else</span> { ItemsHostStackPanel.SetVerticalOffset(index); } IsCategoryViewShown = <span class="code-keyword">false</span>; } } |
Animating the category button ’tiles’
The switch from the jump list to the category view is now a bit more interesting, with a fade-effect applied (which can be replaced by some other effect if you re-template the control). However, it would be much more exciting if each of the category buttons were animated into view, in much the same way that the tiles on the Windows Phone 7 hub are animated.
To support this, the category button template is extended, adding storyboards for showing and hiding the category buttons, in much the same was as the Show()
/ Hide()
extension methods described above. In the example below, a storyboard is defined that shows the category button by scaling and rotating the tile, with the reverse being used to hide it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<span class="code-keyword"><</span><span class="code-leadattribute">Setter</span> <span class="code-attribute">Property</span><span class="code-keyword">="</span><span class="code-keyword">CategoryButtonTemplate"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ControlTemplate</span> <span class="code-attribute">TargetType</span><span class="code-keyword">="</span><span class="code-keyword">Button"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Grid</span> <span class="code-attribute">Background</span><span class="code-keyword">="</span><span class="code-keyword">Transparent"</span> <span class="code-attribute">x:Name</span><span class="code-keyword">="</span><span class="code-keyword">Parent"</span> <span class="code-attribute">RenderTransformOrigin</span><span class="code-keyword">="</span><span class="code-keyword">0.5,0.5"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Grid.Resources</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Storyboard</span> <span class="code-attribute">x:Key</span><span class="code-keyword">="</span><span class="code-keyword">ShowAnim"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DoubleAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">0"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0.2"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">Parent"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">="</span><span class="code-keyword">(UIElement.RenderTransform).( TransformGroup.Children)[0].(RotateTransform.Angle)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DoubleAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">1"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0.2"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">Parent"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">="</span><span class="code-keyword">(UIElement.RenderTransform).( TransformGroup.Children)[1].(ScaleTransform.ScaleX)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DoubleAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">1"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0.2"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">Parent"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">="</span><span class="code-keyword">(UIElement.RenderTransform).( TransformGroup.Children)[1].(ScaleTransform.ScaleY)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Storyboard</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Storyboard</span> <span class="code-attribute">x:Key</span><span class="code-keyword">="</span><span class="code-keyword">HideAnim"</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DoubleAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">120"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0.2"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">Parent"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">="</span><span class="code-keyword">(UIElement.RenderTransform).( TransformGroup.Children)[0].(RotateTransform.Angle)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DoubleAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">0"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0.2"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">Parent"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">="</span><span class="code-keyword">(UIElement.RenderTransform).( TransformGroup.Children)[1].(ScaleTransform.ScaleX)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">DoubleAnimation</span> <span class="code-attribute">To</span><span class="code-keyword">="</span><span class="code-keyword">0"</span> <span class="code-attribute">Duration</span><span class="code-keyword">="</span><span class="code-keyword">0:0:0.2"</span> <span class="code-attribute">Storyboard.TargetName</span><span class="code-keyword">="</span><span class="code-keyword">Parent"</span> <span class="code-attribute">Storyboard.TargetProperty</span><span class="code-keyword">="</span><span class="code-keyword">(UIElement.RenderTransform).( TransformGroup.Children)[1].(ScaleTransform.ScaleY)"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Storyboard</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Grid.Resources</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">Grid.RenderTransform</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">TransformGroup</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">RotateTransform</span> <span class="code-attribute">Angle</span><span class="code-keyword">="</span><span class="code-keyword">120"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-leadattribute">ScaleTransform</span> <span class="code-attribute">ScaleX</span><span class="code-keyword">="</span><span class="code-keyword">0"</span> <span class="code-attribute">ScaleY</span><span class="code-keyword">="</span><span class="code-keyword">0"</span><span class="code-keyword">/</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">TransformGroup</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Grid.RenderTransform</span><span class="code-keyword">></span> ... category button template here ... <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Grid</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">ControlTemplate</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter.Value</span><span class="code-keyword">></span> <span class="code-keyword"><</span><span class="code-keyword">/</span><span class="code-leadattribute">Setter</span><span class="code-keyword">></span> |
BeginTime
property based on the desired delay between the animations for neighboring tiles firing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<span class="code-comment">//</span><span class="code-comment"> sets the begin time for each animation </span><span class="code-keyword">private</span> <span class="code-keyword">static</span> <span class="code-keyword">void</span> PrepareCategoryViewStoryboards(ItemsControl itemsControl, TimeSpan delayBetweenElement) { TimeSpan startTime = <span class="code-keyword">new</span> TimeSpan(<span class="code-digit">0</span>); <span class="code-keyword">var</span> elements = itemsControl.ItemsSource.Cast<FrameworkElement>().ToList(); <span class="code-keyword">foreach</span> (FrameworkElement element <span class="code-keyword">in</span> elements) { <span class="code-keyword">var</span> showStoryboard = GetStoryboardFromRootElement(element, <span class="code-string">"</span><span class="code-string">ShowAnim"</span>); <span class="code-keyword">if</span> (showStoryboard != <span class="code-keyword">null</span>) { showStoryboard.BeginTime = startTime; } <span class="code-keyword">var</span> hideStoryboard = GetStoryboardFromRootElement(element, <span class="code-string">"</span><span class="code-string">HideAnim"</span>); <span class="code-keyword">if</span> (hideStoryboard != <span class="code-keyword">null</span>) { hideStoryboard.BeginTime = startTime; <span class="code-keyword">if</span> (element == elements.Last()) { <span class="code-comment">//</span><span class="code-comment"> when the last animation is complete, hide the ItemsControl </span> hideStoryboard.Completed += (s, e) => { itemsControl.Opacity = <span class="code-digit">0</span>; }; } } startTime = startTime.Add(delayBetweenElement); } } <span class="code-keyword">private</span> <span class="code-keyword">static</span> Storyboard GetStoryboardFromRootElement( FrameworkElement element, <span class="code-keyword">string</span> storyboardName) { FrameworkElement rootElement = element.Elements().Cast<FrameworkElement>().First(); <span class="code-keyword">return</span> rootElement.Resources[storyboardName] <span class="code-keyword">as</span> Storyboard; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<span class="code-comment">//</span><span class="code-comment"> plays the animations associated with each child element </span><span class="code-keyword">public</span> <span class="code-keyword">static</span> <span class="code-keyword">void</span> ShowChildElements(ItemsControl itemsControl, TimeSpan delayBetweenElement) { itemsControl.Opacity = <span class="code-digit">1</span>; PrepareCategoryViewStoryboards(itemsControl, delayBetweenElement); <span class="code-keyword">foreach</span> (FrameworkElement element <span class="code-keyword">in</span> itemsControl.ItemsSource) { <span class="code-keyword">var</span> showStoryboard = GetStoryboardFromRootElement(element, <span class="code-string">"</span><span class="code-string">ShowAnim"</span>); <span class="code-keyword">if</span> (showStoryboard != <span class="code-keyword">null</span>) { showStoryboard.Begin(); } <span class="code-keyword">else</span> { element.Visibility = Visibility.Visible; } } } |
Note that the way this category view animation has been introduced means that the client of the control can change the animation by simply providing an alternative CategoryButtonTemplate
.
Summary
This brings us pretty much to the end of this article. I hope your have enjoyed reading about the development of this control. If you use it in your Windows Phone 7 application, please let me know by leaving a comment below. Also, as mentioned earlier, if you want to read a user-guide or see some more practical examples of this control, please visit my blog.
Today the Windows Phone team released the final bits for Windows Phone 7! The tooling is free and will integrate with Visual Studio 2010 and Expression Blend 4 if you have them installed as well. Head over to
http://www.microsoft.com/downloads/en/details.aspx?FamilyID=04704acf-a63a-4f97-952c-8b51b34b00ce to get the RTM tooling.
Here is also a little list I’ve been keeping with useful controls around the internet!
Controls |
Random Resources |
Silverlight Tool kit
GestureService/GestureListener ContextMenu DatePicker TimePicker ToggleSwitch WrapPanel Wheel Control Swipe Title Picker (Combo box) Progress Bar More Complex splash screen Touch tilting Clearable Textbox Image Caching |
Great list of resources
Detect if a listbox is done scrolling Pass data between pages relative date time converter Phone Styles accent color would be Style=”{StaticResource PhoneAccentBrush}” |