Colin Eberhardt's Adventures in .NET

Automatically Showing ToolTips on a Trimmed TextBlock (Silverlight + WPF)

January 31st, 2011

Both WPF and Silverlight have a property TextTrimming=”WordEllipsis”, which trims the text that a TextBlock displays based on the available width. This blog post describes a simple method for automatically showing the full text as a tooltip whenever the text is trimmed. This is presented as an attached behaviour and works in both WPF and Silverlight

A few weeks ago I blogged about a Silverlight solution for automatically adding tooltips when a TextBlock Text is trimmed and renders an ellipsis. I found a decent looking WPF solutions on the web and linked it in my article, however, based on the comments to my previous blog post, it looks like the WPF solution didn’t work too well, failing to respect font size etc… In this blog post I have updated my solution to be cross-platform, working on WPF and Silverlight.

To briefly recap, my solution for automatically adding tooltips to trimmed text relies on the slightly odd behaviour of the TextBlock where its ActualWidth is reported as the width of the text without trimming:

In order to determine whether to show a tooltip, all you have to do is compare the ActualWidth of the TextBlock to the ActualWidth of its parent.

However, the WPF TextBlock does not have this same quirky behaviour, so a completely different approach is required. The other solutions I have seen involve using the low-level WPF drawing APIs to compute the size of the rendered text, however, there is a simpler solution to this problem …

UPDATE: I originally wrote about a method of finding the overall text width from various internal fields within the TextBlock via reflection, as shown below. However, a kind reader of my blog, Daniel Fihnn, pointed out that there is a simpler solution that does not require any reflection.

Daniel pointed out that the width of the TextBlock without the trimming can be obtained using the Measure method. This method is typically used by panels during layout, the Measure method is invoked with the size made available to the element, calling this method causes the element to update its DesiredSize property. Therefore, if you invoke Measure on a TextBlock which has trimming enabled, giving it an infinite available space, its DesiredSize property will report the width that the text would occupy without trimming.

The ComputeAutoTooltip method of the attached behaviour I described in my previous post is updated to have a completely different WPF implementation:

public class TextBlockUtils
{
  /// <summary>
  /// Gets the value of the AutoTooltipProperty dependency property
  /// </summary>
  public static bool GetAutoTooltip(DependencyObject obj)
  {
    return (bool)obj.GetValue(AutoTooltipProperty);
  }
 
  /// <summary>
  /// Sets the value of the AutoTooltipProperty dependency property
  /// </summary>
  public static void SetAutoTooltip(DependencyObject obj, bool value)
  {
    obj.SetValue(AutoTooltipProperty, value);
  }
 
  /// <summary>
  /// Identified the attached AutoTooltip property. When true, this will set the TextBlock TextTrimming
  /// property to WordEllipsis, and display a tooltip with the full text whenever the text is trimmed.
  /// </summary>
  public static readonly DependencyProperty AutoTooltipProperty = DependencyProperty.RegisterAttached("AutoTooltip",
          typeof(bool), typeof(TextBlockUtils), new PropertyMetadata(false, OnAutoTooltipPropertyChanged));
 
  private static void OnAutoTooltipPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    TextBlock textBlock = d as TextBlock;
    if (textBlock == null)
      return;
 
    if (e.NewValue.Equals(true))
    {
      textBlock.TextTrimming = TextTrimming.WordEllipsis;
      ComputeAutoTooltip(textBlock);
      textBlock.SizeChanged += TextBlock_SizeChanged;
    }
    else
    {
      textBlock.SizeChanged -= TextBlock_SizeChanged;
    }
  }
 
  private static void TextBlock_SizeChanged(object sender, SizeChangedEventArgs e)
  {
    TextBlock textBlock = sender as TextBlock;
    ComputeAutoTooltip(textBlock);
  }
 
  /// <summary>
  /// Assigns the ToolTip for the given TextBlock based on whether the text is trimmed
  /// </summary>
  private static void ComputeAutoTooltip(TextBlock textBlock)
  {
#if WPF
    textBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
    var width = textBlock.DesiredSize.Width;
 
    if (textBlock.ActualWidth < width)
    {
      ToolTipService.SetToolTip(textBlock, textBlock.Text);
    }
    else
    {
      ToolTipService.SetToolTip(textBlock, null);
    }
 
#else
    FrameworkElement parentElement = VisualTreeHelper.GetParent(textBlock) as FrameworkElement;
    if (parentElement != null)
    {
      if (textBlock.ActualWidth > parentElement.ActualWidth)
      {
        ToolTipService.SetToolTip(textBlock, textBlock.Text);
      }
      else
      {
        ToolTipService.SetToolTip(textBlock, null);
      }
    }
#endif 
  }
}

This code now works in both a WPF and a Silverlight context. Here’s a screenshot of it working in WPF:

You can download the full sourcecode, including the WPF and Silverlight demo: AutoTooltipTextBlock.zip

Thanks to Daniel Fihnn for a much improved WPF version of this code!

Regards, Colin E.

Windows Phone 7 DeferredLoadContentControl

January 28th, 2011

This blog post describes a simple content control that can be used to defer the rendering of its contents in order to provide a better user experience on Windows Phone 7.

I think anyone who has made the transition from Emulator to Hardware with developing for Windows Phone 7 has experienced the same performance headaches. a couple of areas where performance hits hard is the rendering of ListBoxes and Images downloaded from the web. Thankfully there are a number of blog posts describing a variety of ways to mitigate the problem. The ListBox performance tips page on the Silverlight WP7 team blog provides links to many useful resources on this topic.

The bottom line is that real WP7 devices take quite a bit longer to render the visual tree, and as developers we have to work around that.

One area I think the user experience really suffers is when the user navigates to a new page. If the page contains a reasonably complex UI (perhaps a list of 100 items), it can take quite a long time for the new page to appear. This gives the impression that the phone has locked up and become unresponsive (which I guess it has to a certain extent!). The guys at Telerik noted that using the Panorama control caused a load time of ~1.6 seconds. Whilst the splash screen will be displayed when you application initially loads, giving the user a feeling that the phone is doing something, subsequent page navigations lack any visual cue to indicate that the phone is doing something and hasn’t just died!

DeferredLoadContentControl

The DeferredLoadContentControl provides a simple solution to this problem, the content that is placed within this control is initially hidden by setting its Visibility to Collapsed. Elements that are collapsed do not occupy any layout space, and hence if an element with a long ‘render’ time is collapsed, this time is no longer consumed. When the DeferredLoadContentControl is initially loaded, it displays a ‘loading…’ indicator, then sets the visibility of its contents to Visible. When the content is rendered, the loading indicator is hidden.

The images below compare the user experience with and without the DeferredLoadContentControl:

Whilst this control does not make the page load any quicker, and the phone cannot be interacted with whilst the content is loading, it does mean that the page itself loads much more quickly and hence gives the user the impression that the phone is more responsive.

The code for the DeferredLoadContentControl is a very simple extension of ContentControl. The template for this control is shown below:

<Style TargetType="local:DeferredLoadContentControl">
  <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
  <Setter Property="verticalContentAlignment" Value="Stretch"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="local:DeferredLoadContentControl">
        <Grid>            
          <ContentPresenter  x:Name="contentPresenter"
                              Content="{TemplateBinding Content}"
                              ContentTemplate="{TemplateBinding ContentTemplate}"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              Margin="{TemplateBinding Padding}"/>
          <TextBlock x:Name="loadingIndicator"
                      Text="Loading ..."
                      VerticalAlignment="Top" HorizontalAlignment="Right"/>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

The template contains a ContentPresenter which is responsible for displaying the controls content, and a TextBlock which displays a discrete message while the content is loading. You can of course re-template this control to add a graphical loading indicator, as long as your element has the name "loadingIndicator" it will be shown / hidden.

The code is also quite simple, here it is in its entirety:

public class DeferredLoadContentControl : ContentControl
{
  private ContentPresenter _contentPresenter;
 
  private FrameworkElement _loadingIndicator;
 
 
  public DeferredLoadContentControl()
  {
    this.DefaultStyleKey = typeof(DeferredLoadContentControl);
 
    if (!DesignerProperties.IsInDesignTool)
    {
      this.Loaded += DeferredLoadContentControl_Loaded;
    }
  }
 
  public override void OnApplyTemplate()
  {
    base.OnApplyTemplate();
 
    _contentPresenter = this.GetTemplateChild("contentPresenter") as ContentPresenter;
    _loadingIndicator = this.GetTemplateChild("loadingIndicator") as FrameworkElement;
 
    if (!DesignerProperties.IsInDesignTool)
    {
      _contentPresenter.Visibility = Visibility.Collapsed;
    }
    else
    {
      // in design-mode show the contents 'grayed out'
      _contentPresenter.Opacity = 0.5;
    }
  }
 
  private void DeferredLoadContentControl_Loaded(object sender, RoutedEventArgs e)
  {
    // content has loaded, now show our content presenter
    _contentPresenter.Visibility = Visibility.Visible;
    _contentPresenter.LayoutUpdated += ContentPresenter_LayoutUpdated;
  }
 
  private void ContentPresenter_LayoutUpdated(object sender, EventArgs e)
  {
    // the content presenter has been rendered, hide the loading indicator
    _contentPresenter.LayoutUpdated -= ContentPresenter_LayoutUpdated;
    _loadingIndicator.Visibility = Visibility.Collapsed;
  }    
}

As you can see from the above, the content presenter is initially hidden, when the loaded event is fired, the content is shown. When the LayoutUpdated event is fired we know that the content has now been rendered and hide the loading indicator.

Using the control couldn’t be simpler. Just place your slow-to-load content inside the DeferredLoadContentControl:

<local:DeferredLoadContentControl>
  ... your content goes here ...
</local:DeferredLoadContentControl>

One nice feature of this control is that when used within the designer, the content contained within the DeferredLoadContentControl is not hidden, instead it is rendered with a 50% and the loading indicator is shown:

You can download the full sourcecode for this control: DeferredLoadContentControl.zip

Regards,
Colin E.

A Navigator Control For Visiblox Time Series Charts

January 26th, 2011

In this blog post I will describe the creation of a simple range selector UserControl, which can be used alongside a Visiblox chart to create an interactive navigator for time series data.

Whether you are studying finance, politics, meteorology or sociology you are sure to encounter time series data. Time series are everywhere! And until the universe starts to collapse in on itself and the arrow of time reverses, these time series are going to keep on growing in size. When charting and exploring large time series datasets, it can help to have a navigator control – a small chart showing the entire dataset at a lower resolution, with controls that allow the user to select a range to view in detail.

This blog post describes how to create a simple navigator control which allows the user to select and drag a time range, as shown below:

The data in the above chart comes from the Time Series Data Library collected and published online by Rob Hyndman.

The Range Control

The markup for the range selector user control is a Grid divided into three columns. The left and right hand column each contain a Border with a blue fill which shades the sections which are outside of the selected range. These cells also each contain a Thumb control, this is the element that the user interacts with. The central column contains a thumb that occupies the entire cell, however, its opacity is set to zero so that it is not visible:

<UserControl x:Class="VisibloxRangeControl.DateTimeRangeControl"
    ... >
 
  <Grid x:Name="LayoutRoot">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="400"/>
      <ColumnDefinition />
      <ColumnDefinition Width="50"/>
    </Grid.ColumnDefinitions>
 
    <Thumb Grid.Column="1"
           Opacity="0"
           Cursor="Hand"
           DragDelta="CentreThumb_DragDelta"
           DragCompleted="Thumb_DragCompleted"/>
 
    <Border Background="Blue"
            BorderBrush="Black"
            BorderThickness="0,0,1,0"
            Opacity="0.3"/>
    <Thumb x:Name="LeftThumb"
           Width="10" Height="20"
           Margin="0,0,-5,0"
           VerticalAlignment="Center" HorizontalAlignment="Right"
           Cursor="SizeWE"
           DragDelta="LeftThumb_DragDelta"
           DragCompleted="Thumb_DragCompleted"/>
 
    <Border Background="Blue"
            BorderThickness="1,0,0,0"
            BorderBrush="Black"
            Opacity="0.3"
            Grid.Column="2"/>
    <Thumb x:Name="RightThumb"
           Grid.Column="2"
           Width="10" Height="20"
           Cursor="SizeWE"
           Margin="-5,0,0,0"
           VerticalAlignment="Center" HorizontalAlignment="Left"
           DragDelta="RightThumb_DragDelta"
           DragCompleted="Thumb_DragCompleted"/>
  </Grid>
</UserControl>

Event handlers are added to the DragDelta and DragCompleted event of each Thumb control. The Thumb control is a bit of an odd one, you might expect that it moves itself as the user clicks and drags it, however, this is not the case. When the user clicks and drags the Thumb, it fires DragDelta events as the mouse moves, however, it is your responsibility to move the Thumb to the updated location to reflect this drag operation.

This might sound odd at first, however, there are many in which a control can be moved, you can set its Canvas location, update its Margin, apply a RenderTransform, to name just a few methods. In the case of the range control described in the XAML above, the Thumb location is dictated by the width of the column that contains it. Therefore, when DragDelta events are fired, we need to update these widths on code-behind, as shown below:

/// <summary>
/// Handles dragging of the left hand thumb control
/// </summary>
private void LeftThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
  // obtain the column width and apply an offset
  var columnDef = LayoutRoot.ColumnDefinitions[0];
  var width = columnDef.Width.Value;
  width += e.HorizontalChange;
 
  // prevent the user from dragging the thumb outside of the control
  if (width < 0)
  {
    width = 0;
  }
 
  // prevent the overlap of the left + right regions
  if (width + LayoutRoot.ColumnDefinitions[2].Width.Value + 20 > this.ActualWidth)
  {
    width = columnDef.Width.Value;
  }
 
  // update the column width
  columnDef.Width = new GridLength(width);
}
 
private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
{
    UpdateExposedBounds();
}

The event handler for the right hand thumb is very similar to the above. The handler for the centre thumb which allows you to drag a region of fixed time is a little more complex, updating the widths of both left and right hand columns, however, the principle is very much the same.

In order to use this control as an interactive range selector, we need to be able to specify the DateTime range it represents and also the control needs to expose the DateTime range for the current selection. To achieve this purpose, the control exposes a Range dependency property of type DateTimeRange (from the Visiblox APIs), which is used to specify the overall date range. The DragCompleted event handler above calls UpdateExposedBounds() which updates a Bounds dependency property to reflect the selected range:

private void UpdateExposedBounds()
{
  if (Range == null)
    return;
 
  double deltaMinutes = (Range.Maximum - Range.Minimum).TotalMinutes;
  double width = this.ActualWidth;
 
  double leftThumbPos = LayoutRoot.ColumnDefinitions[0].Width.Value;
  double rightThumbPos = LayoutRoot.ColumnDefinitions[2].Width.Value;
 
  DateTime upper = Range.Maximum.AddMinutes(-deltaMinutes * (rightThumbPos / width));
  DateTime lower = Range.Minimum.AddMinutes(deltaMinutes * (leftThumbPos / width));
  Bounds = new DateTimeRange(lower, upper);
}

The above code uses a simple bit of algebra to compute the Bounds as a proportion of the overall exposed Range.

Using this range control to create a ‘navigator’ chart and update a ‘detail’ is as simple as binding the range control’s Range property to the XAxis.ActualRange property of the navigator chart, and its Bounds property to the XAxis.Range property of the detail chart.

This can all be achieved via UI bindings as shown below:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="2*" />
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
 
  <!-- the 'detail' chart -->
  <vis:Chart LegendVisibility="Collapsed"
             x:Name="detailChart">
    <vis:Chart.YAxis>
      <vis:LinearAxis LabelsPosition="Inside"
                      AutoScaleToVisibleData="True"/>
    </vis:Chart.YAxis>
    <vis:Chart.XAxis>
      <vis:DateTimeAxis Range="{Binding ElementName=rangeControl, Path=Bounds}"/>
    </vis:Chart.XAxis>
  </vis:Chart>
 
  <!-- the navigator chart -->
  <vis:Chart Grid.Row="1"
             LegendVisibility="Collapsed"
             x:Name="chartNavigator">
    <vis:Chart.XAxis>
      <vis:DateTimeAxis />
    </vis:Chart.XAxis>
  </vis:Chart>
 
  <local:DateTimeRangeControl x:Name="rangeControl"
           Grid.Row="1"
           Range="{Binding ElementName=chartNavigator, Path=XAxis.ActualRange}"/>
</Grid>

You can see the above code in action:

Performance Considerations

With the code above, the range control Bounds property is bound to the X axis range of the upper chart. Each time this range is changed the chart has to perform quite a bit of work, computing the new Y-axis range, re-drawing the series etc… For this reason, the implementation only updates the Bounds property when the user finishes adjusting the range. It would be better if the chart could update whilst the user drags the navigator range. In order to do this, we need a more lightweight method of updating the upper chart.

The Visiblox axes expose a Zoom property which can be used to supply a Scale / Offset which rapidly updates the chart. In order to use the Zoom property we need to convert the Bounds exposed by the range control into a suitable zoom. This is easiest done in code-behind.

The following code handles property changed events from the range control:

private void RangeControl_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  if (rangeControl.Bounds == null)
    return;
 
  DateTimeAxis xAxis = detailChart.XAxis as DateTimeAxis;
 
  double lower = xAxis.GetDataValueAsRenderPositionWithZoom(rangeControl.Bounds.Minimum);
  double upper = xAxis.GetDataValueAsRenderPositionWithZoom(rangeControl.Bounds.Maximum);
  var zoom = xAxis.GetZoom(lower, upper);
  xAxis.Zoom = zoom;
}

The axis exposes a GetZoom method which can be used to create a zoom which will cause the axis to display the data within the given range (in pixels). However, the range control exposes a Bounds which is in the data coordinate system (i.e. dates) rather than the screen coordinate system. Therefore, we first need to apply the GetDataValueAsRenderPositionWithZoom coordinate system conversion method to the upper and lower bound.

With the above method, the chart now updates much more rapidly and we are able to update as the user drags the navigator range:

The Example Data

The data in the examples shows the daily maximum and minimum temperatures in Melbourne from 1981 to 1990. The data was supplied as two separate files, one with maximum temperatures, and the other with minimum temperatures.

These are parsed into a Visiblox DataSeries using the following code:

var assembly = this.GetType().Assembly;
var dataSeries = new DataSeries<DateTime, double>();
var date = new DateTime(1981,1,1);
var maxStream = assembly.GetManifestResourceStream("VisibloxRangeControl.melbmax.dat");
var minStream = assembly.GetManifestResourceStream("VisibloxRangeControl.melbmin.dat");
using (StreamReader minReader = new StreamReader(minStream))
using (StreamReader maxReader = new StreamReader(maxStream))
{
  while (minReader.Peek() > 0)
  {
    string minLine = minReader.ReadLine();
    string maxLine = maxReader.ReadLine();
    var yValues = new Dictionary<object, double>() {
      {BandSeries.Upper, double.Parse(maxLine)},
      {BandSeries.Lower, double.Parse(minLine)}
    };
    dataSeries.Add(new MultiValuedDataPoint<DateTime, double>(date, yValues));
    date = date.AddDays(1);
  }
}
 
detailChart.Series[0].DataSeries = dataSeries;

The detail chart uses this data directly, rendering the max / min values as a band series:

<vis:Chart.Series>
  <vis:BandSeries UpperLineStroke="Blue"
                  LowerLineStroke="Blue"
                  ShowArea="True">
    <vis:BandSeries.AreaFill>
      <SolidColorBrush Color="Blue"
                        Opacity="0.3"/>
    </vis:BandSeries.AreaFill>
  </vis:BandSeries>
</vis:Chart.Series>

Because there are ~3,500 datapoints in this dataset it does not make much sense for the navigator chart to render every point. The following Linq query groups the datapoints by month, then extracts the average upper temperature for each month. This data is then supplied to the lower chart:

var monthlyAverage = dataSeries.GroupBy(pt => new DateTime(pt.X.Year, pt.X.Month, 1))
                            .Select(group => new DataPoint<DateTime, double>
                            {
                              X = group.Key,
                              Y = group.Select(pt => (double)pt[BandSeries.Upper]).Average()
                            });
 
chartNavigator.Series[0].DataSeries = new DataSeries<DateTime, double>(monthlyAverage);

I will never grow tired of the power of Linq!

Final thoughts

The range selector control has been implemented as a UserControl, it could be made more generic by implementing it as a custom control, which would allow it to be templated. Also, it should be possible to make the Range and Bound properties use double rather than DateTime, then use a value converter in the binding to the chart. This would allow range selector to be used in context where the range being selected is not a DateTime one. I’ll leave this as an exercise for the reader!

You can download the full sourcecode here: VisibloxRangeSelector.zip

You will also need to download the free Visiblox charts to compile the code.

Regards, Colin E.

A Windows Phone 7 Jump List Control

January 20th, 2011

This blog post presents a Windows Phone 7 Jump List control that I have developed.This post describes the API in detail and includes full sourcecode. Feel free to use and enjoy!

The video below shows the control working on the emulator, the video further down this page shows it working on a real device. Full sourcecode is linked at the end of this article.

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 blog post describes the API of the Jump List control I have developed. A link to the full sourcecode and a demo application can be found at the end of this blog post.

The video below shows the JumpList working (and performing well!) on a Samsung Omnia WP7. Apologies for the poor video quality!

Basic Example

The API for the JumpList is quite similar to that of the ListBox, it has an ItemsSource property which you set (or bind) the collection of objects you wish to render in the list. The appearance of each item is specified by an ItemTemplate. The example below shows a JumpList bound to a collection of simple Person objects:

public class Person
{
  public string Surname { get; set; }
  public string Forename { get; set; }
}
 
// ItemsSource set in constructor of control ....
list.ItemsSource = PersonDataSource.CreateList(50);

(As an aside, the PersonDataSource above generates pseudo-random names using Markov Chains using some code I found online)

<l:JumpList x:Name="list"
            ScrollDuration="400">
 
  <!-- category provider - details how the items are grouped -->
  <l:JumpList.CategoryProvider>
    <l:AlphabetCategoryProvider PropertyName="Surname"/>
  </l:JumpList.CategoryProvider>
 
  <!-- item template - details how each item is rendered in the list -->
  <l:JumpList.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal"
                  Margin="0,3,0,3"
                  Height="40">
        <Image Source="Head.png"
                Opacity="0.7"
                Width="40" Height="40"/>
        <TextBlock Text="{Binding Surname}"
                    Margin="3,0,0,0"/>
        <TextBlock Text=", "/>
        <TextBlock Text="{Binding Forename}"/>
      </StackPanel>
    </DataTemplate>
  </l:JumpList.ItemTemplate>
 
</l:JumpList>

The main difference between the JumpList and a ListBox is that a JumpList requires that you supply a CategoryProvider. In the example above an AlphabetCategoryProvider was supplied which groups the supplied objects (in this case Person instances) based on the first letter of the property indicated by the PropertyName of the AlphabetCategoryProvider. In this example, the items are being grouped by the Surname property.

The animated scrolling as the list jumps from one category to the next can be controlled via the ScrollDuration property. The animated ‘reveal’ effect as the category buttons are drawn can be controlled via the CategoryTileAnimationDelay property which details the time between the animation being started for neighbouring category buttons.

Note, for very long lists with hundreds of items it makes sense to disable the scrolling effect as the list ‘jumps’ to the selected category.

The jump buttons that appear at the top of each group can be configured via properties that specify their style, template and item template. You can consult the API for more details. The category buttons which are rendered in the category view also have three similar properties. For example, you can use the JumpButtonStyle to make each button the same size, or the CategoryButtonStyle to create larger category buttons.

Selection

The JumpList exposes a SelectedItem property which can be used to get or set the currently selected item. This property also permits TwoWay binding to other controls or a View Model. The JumpList also exposes a SelectionChanged event which is fired whenever the SelectedItem property changed, this event is useful for more programmatic / non-databound usages.

The following code snippet shows a simple UI binding of the JumpList SelectedItem:

<StackPanel DataContext="{Binding Path=SelectedItem, ElementName=list}">
  <TextBlock Text="SELECTED ITEM:"/>
  <!-- displays the Name property of the object bound to the JumpList -->
  <TextBlock Text="{Binding Name}"/>
</StackPanel>
 
<l:JumpList x:Name="list">
  <!-- ItemTemplate etc ... -->
</l:JumpList>

Each item within the JumpList is contained within a JumpListItem. When an item is selected, the default JumpList item template changes the Foreground colour to PhoneAccentBrush. It is possible to style and template the JumpListItems via the JumpListItemStyle property. The following example adds a Border element and sets the Background colour of this Border when an item is selected:

<l:JumpList.JumpListItemStyle>
  <Style TargetType="l:JumpListItem">
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Background" Value="#222"/>
    <Setter Property="Padding" Value="2"/>
    <Setter Property="Margin" Value="0"/>
    <Setter Property="HorizontalContentAlignment" Value="Right"/>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="l:JumpListItem">
          <Border x:Name="LayoutRoot"
                  Background="{TemplateBinding Background}"
                  HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                  VerticalAlignment="{TemplateBinding VerticalAlignment}"
                  BorderBrush="{TemplateBinding BorderBrush}"
                  BorderThickness="{TemplateBinding BorderThickness}">
            <VisualStateManager.VisualStateGroups>
              <VisualStateGroup x:Name="CommonStates">
                <VisualState x:Name="Normal"/>
                <VisualState x:Name="MouseOver" />
                <VisualState x:Name="Disabled"/>
              </VisualStateGroup>
              <VisualStateGroup x:Name="SelectionStates">
                <VisualState x:Name="Unselected"/>
                <VisualState x:Name="Selected">
                  <Storyboard>
                    <!-- change the background on selection -->
                    <ColorAnimation
                        Storyboard.TargetName="LayoutRoot"
                        Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                        To="#555"
                        Duration="0:0:0.5">
                    </ColorAnimation>
                  </Storyboard>
                </VisualState>
              </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
 
            <ContentControl x:Name="ContentContainer"
                      VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                      HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                      Margin="{TemplateBinding Padding}"
                      Content="{TemplateBinding Content}"
                      ContentTemplate="{TemplateBinding ContentTemplate}"
                      Foreground="{TemplateBinding Foreground}" />
          </Border>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</l:JumpList.JumpListItemStyle>

The Category Provider

As seen in the earlier example, the JumpList uses a category provider, specified via the JumpList.CategoryProvider property to place items into categories within the jump list. The CategoryProvider implements the following interface:

/// <summary>
/// A category provider assigns items to categories and details
/// the full category list for a set of items.
/// </summary>
public interface ICategoryProvider
{
  /// <summary>
  /// Gets the category for the given items
  /// </summary>
  object GetCategoryForItem(object item);
 
  /// <summary>
  /// Gets the full list of categories for the given items.
  /// </summary>
  List<object> GetCategoryList(IEnumerable items);
}

The GetCategoryForItem method is used to determine the category for each item in the list, whilst GetCategoryList returns the full list of categories in order to display the category view.

The JumpList comes with a couple of ‘built in’ category providers:

The AlphabetCategoryProvider uses the first letter of a named property to identify the category for each item. The category list which AlphabetCategoryProvider provides is always the full alphabet. The category view ‘grays’ category buttons for which the source list has no items.

The DistinctPropertyValueCategoryProvider is a slightly more generic provider, placing items into categories based on the value of the property nominated via PropertyName. With this provider the category list displays all the unique or distinct values found for the given property.

For example, if you have a list of events which you want to group by month, you can use a DistinctPropertyValueCategoryProvider identifying a property on your object which indicates the month. For example:

public class JugglingEvent
{
  public string Name { get; set; }
  public DateTime Date { get; set; }
  public string Description { get; set; }
 
  public int Month
  {
    get
    {
      return Date.Month;
    }
  }
}

The category provider is detailed as follows:

<!-- category provider groups by month-->
<l:JumpList.CategoryProvider>
  <l:DistinctPropertyValueCategoryProvider
        PropertyName="Month"/>
</l:JumpList.CategoryProvider>

This will group the items by month. However, the Month property exposes an integer, and this will be supplied as the DataContext for the jump buttons and category buttons. In order to display the month indices as their respective strings, a value converter can be supplied to the jump button and category button item templates:

<!-- the jump button template renders the month as a string -->
<l:JumpList.JumpButtonItemTemplate>
  <DataTemplate>
    <TextBlock Text="{Binding Path=., Converter={StaticResource MonthIndexToStringConverter}}"/>
  </DataTemplate>
</l:JumpList.JumpButtonItemTemplate>
 
<!-- the category template renders the month as a string -->
<l:JumpList.CategoryButtonItemTemplate>
  <DataTemplate>
    <TextBlock Text="{Binding Path=., Converter={StaticResource MonthIndexToStringConverter}}"/>
  </DataTemplate>
</l:JumpList.CategoryButtonItemTemplate>

Custom Category Button Animations

When the category view is displayed, each category button is revealed via a storyboard. These storyboards are started sequentially with a delay, specified by the CategoryTileAnimationDelay property, between the start of neighbouring buttons.

The storyboards that define the animation are within the template of the category buttons. The JumpList control looks for two storyboards, name “ShowAnim” and “HideAnim”, and triggers these to show / hide the button respectively.

If you want to disable animations altogether, you can omit these storyboards. You can also specify your a custom animation by inserting your own storyboards into the category button template.

The example below adds a custom animation which reveals each button by rotating it around its centre:

<!-- create a custom category button animation -->
<l:JumpList.CategoryButtonTemplate>
  <ControlTemplate TargetType="Button">
    <Grid Background="Transparent"
        x:Name="Parent"
        RenderTransformOrigin="0.5,0.5">
      <Grid.Resources>
        <Storyboard x:Key="ShowAnim">
          <DoubleAnimation To="0" Duration="0:0:0.3"
                            Storyboard.TargetName="Parent"
                            Storyboard.TargetProperty="(Grid.Projection).(PlaneProjection.RotationX)"/>
        </Storyboard>
        <Storyboard x:Key="HideAnim">
          <DoubleAnimation To="90" Duration="0:0:0.1"
                            Storyboard.TargetName="Parent"
                            Storyboard.TargetProperty="(Grid.Projection).(PlaneProjection.RotationX)"/>
        </Storyboard>
      </Grid.Resources>
      <Grid.Projection>
        <PlaneProjection RotationX="90"/>
      </Grid.Projection>
 
      <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="CommonStates">
          <VisualState x:Name="Normal"/>
          <VisualState x:Name="MouseOver"/>
          <VisualState x:Name="Pressed">
            <Storyboard>
              <ColorAnimation To="White" Duration="0:0:0"
                            Storyboard.TargetName="Background"
                            Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)"/>
            </Storyboard>
          </VisualState>
          <VisualState x:Name="Disabled">
            <Storyboard>
              <DoubleAnimation To="1" Duration="0:0:0"
                            Storyboard.TargetName="DisabledBackground"
                            Storyboard.TargetProperty="(Rectangle.Opacity)"/>
              <DoubleAnimation To="0" Duration="0:0:0"
                            Storyboard.TargetName="Background"
                            Storyboard.TargetProperty="(Rectangle.Opacity)"/>
            </Storyboard>
          </VisualState>
        </VisualStateGroup>
      </VisualStateManager.VisualStateGroups>
      <Rectangle  x:Name="Background"
                      Fill="{StaticResource PhoneAccentBrush}"/>
      <Rectangle  x:Name="DisabledBackground"
                      Fill="{StaticResource PhoneBackgroundBrush}"
                      Opacity="0"/>
      <ContentControl x:Name="ContentContainer"
                    Foreground="{TemplateBinding Foreground}"
                    HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                    VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                    Padding="{TemplateBinding Padding}"
                    Content="{TemplateBinding Content}"
                    ContentTemplate="{TemplateBinding ContentTemplate}"/>
    </Grid>
  </ControlTemplate>
</l:JumpList.CategoryButtonTemplate>

If you have many categories, and want to disable the ‘reveal’ animation, simply supply a template without any storyboards, see the demo sourcecode for an example.

You can download the full sourcecode: WP7JumpList.zip

To run this code you will also need the Windows Phone 7 Toolkit.

If you have any feedback, ideas or bugs to report – please let me know!

Regards, Colin E.