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
}
}