Click here to Skip to main content
15,895,746 members
Articles / Mobile Apps / Windows Phone 7

Developing a Windows Phone 7 Jump List Control

Rate me:
Please Sign up or sign in to vote.
4.95/5 (29 votes)
18 May 2011CPOL18 min read 96.6K   2.2K   46  
This article describes the development of a Windows Phone 7 Jump List control, giving a step-by-step account of the control's development (and a pretty flashy control to use at the end of it!).
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Animation;
using CodeGen;
using LinqToVisualTree;
using System.Collections.Specialized;

namespace JumpListControl
{
  /// <summary>
  /// A list control for presenting long lists of data
  /// </summary>
  [DependencyPropertyDecl("ItemsSource", typeof(IEnumerable), null, "Gets or sets a collection used to generate the content of the JumpList")]
  [DependencyPropertyDecl("ItemTemplate", typeof(DataTemplate), null, "Gets or sets the DataTemplate used to display each item")]
  [DependencyPropertyDecl("JumpButtonItemTemplate", typeof(DataTemplate), null, "Gets or sets the DataTemplate used to display the Jump buttons. The DataContext of each button is a group key")]
  [DependencyPropertyDecl("JumpButtonTemplate", typeof(ControlTemplate), null, "Gets or sets the ControlTemplate for the Jump buttons")]
  [DependencyPropertyDecl("CategoryButtonItemTemplate", typeof(DataTemplate), null, "Gets or sets the DataTemplate used to display the Category buttons. The DataContext of each Category button is an item from the ICategoryProvider.GetCategoryList list")]
  [DependencyPropertyDecl("CategoryButtonTemplate", typeof(ControlTemplate), null, "Gets or sets the ControlTemplate for the Category buttons")]
  [DependencyPropertyDecl("CategoryProvider", typeof(ICategoryProvider), null, "Gets or sets a category provider which groups the items in the JumpList and specifies the categories in the jump menu")]
  [DependencyPropertyDecl("JumpButtonStyle", typeof(Style), null, "Gets or sets the style applied to the Jump buttons. This should be a style with a TargetType of Button")]
  [DependencyPropertyDecl("CategoryButtonStyle", typeof(Style), null, "Gets or sets the style applied to the Category buttons. This should be a style with a TargetType of Button")]
  [DependencyPropertyDecl("CategoryTileAnimationDelay", typeof(double), 20.0, "Gets or sets the time delay in milliseconds between firing the animations which reveal the Category button")]
  [DependencyPropertyDecl("ScrollDuration", typeof(double), 200.0, "Gets or sets the time taken to 'jump' to a new list location in milliseconds")]
  [DependencyPropertyDecl("SelectedItem", typeof(object), null, "Gets or sets the selected item")]
  [DependencyPropertyDecl("IsCategoryViewShown", typeof(bool), false, "Gets or sets whether the category view is currently shown")]
  [DependencyPropertyDecl("JumpListItemStyle", typeof(Style), null, "Gets or sets the style applied to each jump list item")]
  public partial class JumpList : Control, INotifyPropertyChanged
  {
    #region fields

    private ItemsControl _categoryItemsControl;

    private ItemsControl _jumpListControl;

    private FrameworkElement _loadingIndicator;

    private VirtualizingStackPanel _stackPanel;

    private List<object> _categoryList;

    private DoubleAnimation _scrollAnimation;

    private Storyboard _scrollStoryboard;

    private List<object> _flattenedCategories;

    #endregion

    #region VerticalOffset DP

    /// <summary>
    /// VerticalOffset, a private DP used to animate the scrollviewer
    /// </summary>
    private DependencyProperty VerticalOffsetProperty = DependencyProperty.Register("VerticalOffset",
      typeof(double), typeof(JumpList), new PropertyMetadata(0.0, OnVerticalOffsetChanged));

    private static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      JumpList jumpList = d as JumpList;
      jumpList.OnVerticalOffsetChanged(e);
    }

    private void OnVerticalOffsetChanged(DependencyPropertyChangedEventArgs e)
    {
      ItemsHostStackPanel.SetVerticalOffset((double)e.NewValue);
    }

    #endregion

    #region DP change handlers

    partial void OnJumpListItemStylePropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    partial void OnScrollDurationPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      // no-op
    }

    partial void OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      if ((bool)e.NewValue == true)
      {
        // first time load!
        if (_categoryItemsControl.Visibility == Visibility.Collapsed)
        {
          _loadingIndicator.Opacity = 1;
          _jumpListControl.Hide();

          Dispatcher.BeginInvoke(() =>
          {
            _categoryItemsControl.LayoutUpdated += new EventHandler(CategoryItemsControl_LayoutUpdated);
            _categoryItemsControl.Visibility = Visibility.Visible;
          });
        }
        else
        {
          _jumpListControl.Hide();
          _jumpListControl.IsHitTestVisible = false;
          _categoryItemsControl.IsHitTestVisible = true;
          ShowChildElements(_categoryItemsControl, TimeSpan.FromMilliseconds(CategoryTileAnimationDelay));
        }
      }
      else
      {
        _jumpListControl.Show();
        _jumpListControl.IsHitTestVisible = true;
        _categoryItemsControl.IsHitTestVisible = false;
        HideChildElements(_categoryItemsControl, TimeSpan.FromMilliseconds(CategoryTileAnimationDelay));
      }
    }

    partial void OnCategoryButtonTemplatePropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    partial void OnCategoryButtonStylePropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    partial void OnCategoryProviderPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    partial void OnCategoryButtonItemTemplatePropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    partial void OnCategoryTileAnimationDelayPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      // no op
    }

    partial void OnItemTemplatePropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    partial void OnJumpButtonStylePropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    partial void OnJumpButtonTemplatePropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    partial void OnItemsSourcePropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      INotifyCollectionChanged oldIncc = e.OldValue as INotifyCollectionChanged;
      if (oldIncc != null)
      {
        oldIncc.CollectionChanged -= ItemsSource_CollectionChanged;
      }

      INotifyCollectionChanged incc = e.NewValue as INotifyCollectionChanged;
      if (incc != null)
      {
        incc.CollectionChanged += ItemsSource_CollectionChanged;
      }

      RebuildCategorisedList();
    }

    partial void OnSelectedItemPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      var previousSelection = e.OldValue;

      // de-select the previous item
      var oldSelectedJumpListItem = FlattenedCategories.OfType<JumpListItem>()
                                                       .Where(i => i.Tag.Equals(previousSelection))
                                                       .SingleOrDefault();
      if (oldSelectedJumpListItem != null)
      {
        VisualStateManager.GoToState(oldSelectedJumpListItem, "Unselected", true);
      }

      // select the new item
      var newSelectedItem = FlattenedCategories.OfType<JumpListItem>()
                                                       .Where(i => i.Tag.Equals(SelectedItem))
                                                       .SingleOrDefault();
      if (newSelectedItem != null)
      {
        VisualStateManager.GoToState(newSelectedItem, "Selected", true);
      }

      // raise the selection changed event
      OnSelectionChanged(new SelectionChangedEventArgs(ToList(e.OldValue), ToList(e.NewValue)));
    }

    private IList ToList(object item)
    {
      IList asList = new List<object>();
      if (item != null)
        asList.Add(item);
      return asList;
    }


    #endregion

    #region properties

    /// <summary>
    /// Gets the categorised list of items
    /// </summary>
    public List<object> FlattenedCategories
    {
      get
      {
        return _flattenedCategories;
      }
      private set
      {
        _flattenedCategories = value;
        OnPropertyChanged("FlattenedCategories");
      }
    }

    /// <summary>
    /// Gets the stack panel that hosts our jump list items
    /// </summary>
    private VirtualizingStackPanel ItemsHostStackPanel
    {
      get
      {
        if (_stackPanel == null)
        {
          _stackPanel = _jumpListControl.Descendants<VirtualizingStackPanel>()
                                     .Cast<VirtualizingStackPanel>()
                                     .SingleOrDefault();
        }

        return _stackPanel;
      }
    }

    /// <summary>
    /// Gets a list of categories
    /// </summary>
    public List<object> CategoryList
    {
      get
      {
        return _categoryList;
      }
      private set
      {
        _categoryList = value;
        OnPropertyChanged("CategoryList");
      }
    }

    #endregion

    #region public API

    /// <summary>
    /// Occurs when the selection changes.
    /// </summary>
    public event SelectionChangedEventHandler SelectionChanged;

    public JumpList()
    {
      DefaultStyleKey = typeof(JumpList);
      RebuildCategorisedList();

      // create a scroll animation
      _scrollAnimation = new DoubleAnimation();
      _scrollAnimation.EasingFunction = new SineEase();

      // create a storyboard for the animation
      _scrollStoryboard = new Storyboard();
      _scrollStoryboard.Children.Add(_scrollAnimation);
      Storyboard.SetTarget(_scrollAnimation, this);
      Storyboard.SetTargetProperty(_scrollAnimation, new PropertyPath("VerticalOffset"));

      // Make the Storyboard a resource.
      Resources.Add("anim", _scrollStoryboard);
    }

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      _jumpListControl = this.GetTemplateChild("JumpListItems") as ItemsControl;
      _loadingIndicator = this.GetTemplateChild("LoadingIndicator") as FrameworkElement;
      _categoryItemsControl = this.GetTemplateChild("CategoryItems") as ItemsControl;
    }

    #endregion

    #region private methods

    private void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      RebuildCategorisedList();
    }

    /// <summary>
    /// Handles LayoutUpdated event in order to hide the loading indicator
    /// </summary>
    private void CategoryItemsControl_LayoutUpdated(object sender, EventArgs e)
    {
      _categoryItemsControl.LayoutUpdated -= CategoryItemsControl_LayoutUpdated;

      _loadingIndicator.Visibility = System.Windows.Visibility.Collapsed;

      Dispatcher.BeginInvoke(() =>
      {
        ShowChildElements(_categoryItemsControl, TimeSpan.FromMilliseconds(CategoryTileAnimationDelay));
      });
    }

    /// <summary>
    /// Raises the SelectionChanged event
    /// </summary>
    protected void OnSelectionChanged(SelectionChangedEventArgs args)
    {
      if (SelectionChanged != null)
      {
        SelectionChanged(this, args);
      }
    }


    private void CategoryButton_Click(object sender, RoutedEventArgs e)
    {
      var categoryButton = sender as Button;

      // find the jump button for this category 
      var button = FlattenedCategories.OfType<Button>()
                                      .Where(b => b.Content.Equals(categoryButton.Content))
                                      .SingleOrDefault();

      // button is null if there are no items in the clicked category
      if (button != null)
      {
        // find the button index
        var index = FlattenedCategories.IndexOf(button);

        if (ScrollDuration > 0.0)
        {
          _scrollAnimation.Duration = TimeSpan.FromMilliseconds(ScrollDuration);
          _scrollStoryboard.Duration = TimeSpan.FromMilliseconds(ScrollDuration);
          _scrollAnimation.To = (double)index;
          _scrollAnimation.From = ItemsHostStackPanel.ScrollOwner.VerticalOffset;
          _scrollStoryboard.Begin();
        }
        else
        {
          ItemsHostStackPanel.SetVerticalOffset(index);
        }

        IsCategoryViewShown = false;
      }
    }

    /// <summary>
    /// Creates a categorised list of items, together with the category view items source
    /// </summary>
    private void RebuildCategorisedList()
    {
      if (ItemsSource == null)
        return;

      // adds each item into a category
      var categorisedItemsSource = ItemsSource.Cast<object>()
                                              .GroupBy(i => CategoryProvider.GetCategoryForItem(i))
                                              .OrderBy(g => g.Key)
                                              .ToList();
      
     
      // create the jump list
      var jumpListItems = new List<object>();
      foreach (var category in categorisedItemsSource)
      {
        jumpListItems.Add(new Button()
        {
          Content = category.Key,
          ContentTemplate = JumpButtonItemTemplate,
          Template = JumpButtonTemplate,
          Style = JumpButtonStyle
        });
        jumpListItems.AddRange(category.Select(item =>
         new JumpListItem()
         {
           Content = item,
           Tag = item,
           ContentTemplate = ItemTemplate,
           Style = JumpListItemStyle
         }).Cast<object>());
      }

      // add interaction handlers
      foreach (var button in jumpListItems.OfType<Button>())
      {
        button.Click += JumpButton_Click;
      }
      foreach (var item in jumpListItems.OfType<JumpListItem>())
      {
        item.MouseLeftButtonUp += JumpListItem_MouseLeftButtonUp;
      }

      FlattenedCategories = jumpListItems;

      // creates the category view, where the active state is determined by whether
      // there are any items in the category
      CategoryList = CategoryProvider.GetCategoryList(ItemsSource)
                                     .Select(category => new Button()
                                     {
                                       Content = category,
                                       IsEnabled = categorisedItemsSource.Any(categoryItems => categoryItems.Key.Equals(category)),
                                       ContentTemplate = this.CategoryButtonItemTemplate,
                                       Style = this.CategoryButtonStyle,
                                       Template = this.CategoryButtonTemplate
                                     }).Cast<object>().ToList();

      foreach (var button in CategoryList.OfType<Button>())
      {
        button.Click += CategoryButton_Click;
      }
    }

    private void JumpListItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
      var jumpListItem = sender as JumpListItem;
      var newSelectedItem = jumpListItem.Tag;
      SelectedItem = newSelectedItem;
    }

    private void JumpButton_Click(object sender, RoutedEventArgs e)
    {
      IsCategoryViewShown = true;
    }

    #endregion

    #region INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string property)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(property));
      }
    }

    #endregion

    /// <summary>
    /// Gets an ancestor with the given name
    /// </summary>
    private static T GetNamedAncestor<T>(FrameworkElement element, string name)
      where T : FrameworkElement
    {
      return element.AncestorsAndSelf<T>()
                   .Cast<T>()
                   .Where(a => a.Name == name)
                   .SingleOrDefault();
    }


    private static Storyboard GetStoryboardFromRootElement(FrameworkElement element, string storyboardName)
    {
      FrameworkElement rootElement = element.Elements().Cast<FrameworkElement>().First();
      return rootElement.Resources[storyboardName] as Storyboard;
    }

    private static void PrepareCategoryViewStoryboards(ItemsControl itemsControl, TimeSpan delayBetweenElement)
    {
      TimeSpan startTime = new TimeSpan(0);
      var elements = itemsControl.ItemsSource.Cast<FrameworkElement>().ToList();
      foreach (FrameworkElement element in elements)
      {
        var showStoryboard = GetStoryboardFromRootElement(element, "ShowAnim");
        if (showStoryboard != null)
        {
          showStoryboard.BeginTime = startTime;
        }

        var hideStoryboard = GetStoryboardFromRootElement(element, "HideAnim");
        if (hideStoryboard != null)
        {
          hideStoryboard.BeginTime = startTime;

          if (element == elements.Last())
          {
            hideStoryboard.Completed += (s, e) =>
            {
              itemsControl.Opacity = 0;
            };
          }
        }

        startTime = startTime.Add(delayBetweenElement);
      }
    }

    public static void ShowChildElements(ItemsControl itemsControl, TimeSpan delayBetweenElement)
    {
      itemsControl.Opacity = 1;
      PrepareCategoryViewStoryboards(itemsControl, delayBetweenElement);
      foreach (FrameworkElement element in itemsControl.ItemsSource)
      {
        var showStoryboard = GetStoryboardFromRootElement(element, "ShowAnim");
        if (showStoryboard != null)
        {
          showStoryboard.Begin();
        }
        else
        {
          element.Visibility = Visibility.Visible;
        }
      }
    }

    public static void HideChildElements(ItemsControl itemsControl, TimeSpan delayBetweenElement)
    {
      PrepareCategoryViewStoryboards(itemsControl, delayBetweenElement);
      foreach (FrameworkElement element in itemsControl.ItemsSource)
      {
        var hideStoryboard = GetStoryboardFromRootElement(element, "HideAnim");
        if (hideStoryboard != null)
        {
          hideStoryboard.Begin();
        }
        else
        {
          element.Visibility = Visibility.Collapsed;
        }
      }
    }

    #region value objects

    public class CategoryHeading
    {
      public object Category { get; set; }
      public bool IsActive { get; set; }
      public DataTemplate CategoryButtonItemTemplate { get; set; }
      public Style CategoryButtonStyle { get; set; }
      public ControlTemplate CategoryButtonTemplate { get; set; }
    }

    public class CategoryItems
    {
      public object Category { get; set; }
      public Style JumpButtonStyle { get; set; }
      public DataTemplate JumpButtonItemTemplate { get; set; }
      public ControlTemplate JumpButtonTemplate { get; set; }
      public DataTemplate ItemTemplate { get; set; }
      public List<object> Items { get; set; }
    }

    #endregion
  }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Architect Scott Logic
United Kingdom United Kingdom
I am CTO at ShinobiControls, a team of iOS developers who are carefully crafting iOS charts, grids and controls for making your applications awesome.

I am a Technical Architect for Visiblox which have developed the world's fastest WPF / Silverlight and WP7 charts.

I am also a Technical Evangelist at Scott Logic, a provider of bespoke financial software and consultancy for the retail and investment banking, stockbroking, asset management and hedge fund communities.

Visit my blog - Colin Eberhardt's Adventures in .NET.

Follow me on Twitter - @ColinEberhardt

-

Comments and Discussions