Browse Source

Started refactoring SelectingItemsControl.

pull/69/head
Steven Kirk 11 years ago
parent
commit
ff331693f0
  1. 6
      Perspex.Base/PerspexProperty.cs
  2. 68
      Perspex.Controls/Generators/IItemContainerGenerator.cs
  3. 231
      Perspex.Controls/Generators/ItemContainerGenerator.cs
  4. 37
      Perspex.Controls/Generators/ItemContainers.cs
  5. 139
      Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  6. 2
      Perspex.Controls/Generators/TypedItemContainerGenerator.cs
  7. 22
      Perspex.Controls/ItemsControl.cs
  8. 5
      Perspex.Controls/Menu.cs
  9. 6
      Perspex.Controls/MenuItem.cs
  10. 1
      Perspex.Controls/Perspex.Controls.csproj
  11. 20
      Perspex.Controls/Presenters/DeckPresenter.cs
  12. 11
      Perspex.Controls/Presenters/ItemsPresenter.cs
  13. 343
      Perspex.Controls/Primitives/SelectingItemsControl.cs
  14. 18
      Perspex.Controls/Primitives/TabStrip.cs
  15. 27
      Perspex.Controls/Templates/DataTemplateExtensions.cs
  16. 260
      Perspex.Controls/TreeView.cs
  17. 6
      Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  18. 74
      Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

6
Perspex.Base/PerspexProperty.cs

@ -77,8 +77,12 @@ namespace Perspex
this.defaultValues.Add(ownerType, defaultValue);
this.Inherits = inherits;
this.DefaultBindingMode = defaultBindingMode;
this.validation.Add(ownerType, validate);
this.IsAttached = isAttached;
if (validate != null)
{
this.validation.Add(ownerType, validate);
}
}
/// <summary>

68
Perspex.Controls/Generators/IItemContainerGenerator.cs

@ -1,6 +1,6 @@
// -----------------------------------------------------------------------
// <copyright file="IItemContainerGenerator.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
@ -9,30 +9,60 @@ namespace Perspex.Controls.Generators
using System;
using System.Collections;
using System.Collections.Generic;
using Templates;
public enum ItemContainerGeneratorState
{
NoStarted,
Generating,
Generated,
}
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// </summary>
public interface IItemContainerGenerator
{
event EventHandler StateChanged;
ItemContainerGeneratorState State { get; }
Control GetContainerForItem(object item);
/// <summary>
/// Signalled whenever new containers are initialized.
/// </summary>
IObservable<ItemContainers> ContainersInitialized { get; }
object GetItemForContainer(Control container);
/// <summary>
/// Creates container controls for a collection of items.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="items">The items.</param>
/// <param name="itemTemplate">An optional item template.</param>
/// <returns>The created controls.</returns>
IList<IControl> CreateContainers(
int startingIndex,
IEnumerable items,
IDataTemplate itemTemplate);
IEnumerable<Tuple<object, Control>> GetAll();
/// <summary>
/// Removes a set of created containers from the index and returns the removed controls.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="count">The number of items to remove.</param>
/// <returns>The removed controls.</returns>
IList<IControl> RemoveContainers(int startingIndex, int count);
IEnumerable<Control> Generate(IEnumerable items);
/// <summary>
/// Clears the created containers from the index and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IList<IControl> ClearContainers();
IEnumerable<Control> Remove(IEnumerable item);
/// <summary>
/// Gets the container control representing the item with the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The container, or null if no container created.</returns>
IControl ContainerFromIndex(int index);
void RemoveAll();
/// <summary>
/// Gets the index of the specified container control.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The index of the container, or -1 if not found.</returns>
int IndexFromContainer(IControl container);
}
}
}

231
Perspex.Controls/Generators/ItemContainerGenerator.cs

@ -1,6 +1,6 @@
// -----------------------------------------------------------------------
// <copyright file="ItemContainerGenerator.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
@ -10,145 +10,182 @@ namespace Perspex.Controls.Generators
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Perspex.Controls.Templates;
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
{
private Dictionary<object, Control> containersByItem = new Dictionary<object, Control>();
private Dictionary<int, IControl> containers = new Dictionary<int, IControl>();
private Dictionary<Control, object> itemsByContainer = new Dictionary<Control, object>();
private Subject<ItemContainers> containersInitialized;
private ItemContainerGeneratorState state;
public ItemContainerGenerator(Control owner)
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public ItemContainerGenerator(IControl owner)
{
this.Owner = owner;
this.containersInitialized = new Subject<ItemContainers>();
}
public event EventHandler StateChanged;
public ItemContainerGeneratorState State
/// <summary>
/// Signalled whenever new containers are initialized.
/// </summary>
public IObservable<ItemContainers> ContainersInitialized => this.containersInitialized;
/// <summary>
/// Gets the owner control.
/// </summary>
public IControl Owner { get; }
/// <summary>
/// Creates container controls for a collection of items.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="items">The items.</param>
/// <param name="itemTemplate">An optional item template.</param>
/// <returns>The created container controls.</returns>
public IList<IControl> CreateContainers(
int startingIndex,
IEnumerable items,
IDataTemplate itemTemplate)
{
get
Contract.Requires<ArgumentNullException>(items != null);
int index = startingIndex;
var result = new List<IControl>();
foreach (var item in items)
{
return this.state;
IControl container = this.CreateContainer(item, itemTemplate);
result.Add(container);
}
private set
{
if (this.state != value)
{
this.state = value;
this.AddContainers(startingIndex, result);
this.containersInitialized.OnNext(new ItemContainers(startingIndex, result));
if (this.StateChanged != null)
{
this.StateChanged(this, EventArgs.Empty);
}
}
}
return result.Where(x => x != null).ToList();
}
protected Control Owner
/// <summary>
/// Removes a set of created containers from the index and returns the removed controls.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// </param>
/// <param name="count">The number of items to remove.</param>
/// <returns>The removed controls.</returns>
public IList<IControl> RemoveContainers(int startingIndex, int count)
{
get;
private set;
}
var result = new List<IControl>();
for (int i = startingIndex; i < startingIndex + count; ++i)
{
var container = this.containers[i];
if (container != null)
{
result.Add(container);
this.containers[i] = null;
}
}
public Control GetContainerForItem(object item)
{
Control result;
this.containersByItem.TryGetValue(item, out result);
return result;
}
public object GetItemForContainer(Control container)
/// <summary>
/// Clears the created containers from the index and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
public IList<IControl> ClearContainers()
{
object result;
this.itemsByContainer.TryGetValue(container, out result);
return result;
var result = this.containers;
this.containers = new Dictionary<int, IControl>();
return result.Values.ToList();
}
public IEnumerable<Tuple<object, Control>> GetAll()
/// <summary>
/// Gets the container control representing the item with the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The container or null if no container created.</returns>
public IControl ContainerFromIndex(int index)
{
return this.containersByItem.Select(x => Tuple.Create(x.Key, x.Value));
IControl result;
this.containers.TryGetValue(index, out result);
return result;
}
IEnumerable<Control> IItemContainerGenerator.Generate(IEnumerable items)
/// <summary>
/// Gets the index of the specified container control.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The index of the container or -1 if not found.</returns>
public int IndexFromContainer(IControl container)
{
List<Control> result = new List<Control>();
this.State = ItemContainerGeneratorState.Generating;
try
foreach (var i in this.containers)
{
foreach (object item in items)
if (i.Value == container)
{
Control container = this.CreateContainerOverride(item);
if (container != null)
{
if (container.DataContext == null)
{
container.DataContext = item;
}
container.TemplatedParent = null;
this.AddInternal(item, container);
result.Add(container);
}
return i.Key;
}
}
finally
{
this.State = ItemContainerGeneratorState.Generated;
}
return result;
return -1;
}
IEnumerable<Control> IItemContainerGenerator.Remove(IEnumerable items)
/// <summary>
/// Creates the container for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="itemTemplate">An optional item template.</param>
/// <returns>The created container control.</returns>
protected virtual IControl CreateContainer(object item, IDataTemplate itemTemplate)
{
List<Control> result = new List<Control>();
foreach (var item in items)
if (item == null)
{
result.Add(this.RemoveByItemInternal(item));
return null;
}
else if (itemTemplate != null && itemTemplate.Match(item))
{
var result = itemTemplate.Build(item);
result.DataContext = item;
return result;
}
else
{
return this.Owner.MaterializeDataTemplate(item);
}
return result;
}
void IItemContainerGenerator.RemoveAll()
{
this.containersByItem.Clear();
this.itemsByContainer.Clear();
}
protected virtual Control CreateContainerOverride(object item)
{
return (Control)this.Owner.MaterializeDataTemplate(item);
}
protected void AddInternal(object item, Control container)
/// <summary>
/// Adds a collection of containers to the index.
/// </summary>
/// <param name="index">The starting index.</param>
/// <param name="container">The container.</param>
protected void AddContainers(int index, IList<IControl> container)
{
this.containersByItem.Add(item, container);
this.itemsByContainer.Add(container, item);
}
Contract.Requires<ArgumentNullException>(container != null);
protected object RemoveByContainerInternal(Control container)
{
object item = this.itemsByContainer[container];
this.containersByItem.Remove(item);
this.itemsByContainer.Remove(container);
return item;
}
foreach (var c in container)
{
if (!this.containers.ContainsKey(index))
{
this.containers[index] = c;
}
else
{
throw new InvalidOperationException("Container already created.");
}
protected Control RemoveByItemInternal(object item)
{
Control container = this.containersByItem[item];
this.containersByItem.Remove(item);
this.itemsByContainer.Remove(container);
return container;
++index;
}
}
}
}
}

37
Perspex.Controls/Generators/ItemContainers.cs

@ -0,0 +1,37 @@
// -----------------------------------------------------------------------
// <copyright file="ItemContainers.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls.Generators
{
using System.Collections.Generic;
/// <summary>
/// Holds details about a set of item containers in an <see cref="IItemContainerGenerator"/>.
/// </summary>
public class ItemContainers
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainers"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="containers">The containers.</param>
public ItemContainers(int startingIndex, IList<IControl> containers)
{
this.StartingIndex = startingIndex;
this.Items = containers;
}
/// <summary>
/// Gets the index of the first container in the source items.
/// </summary>
public int StartingIndex { get; }
/// <summary>
/// Gets the containers. May contain null entries.
/// </summary>
public IList<IControl> Items { get; }
}
}

139
Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@ -19,74 +19,75 @@ namespace Perspex.Controls.Generators
{
}
IEnumerable<Control> IItemContainerGenerator.Remove(IEnumerable items)
{
var result = new List<Control>();
foreach (var item in items)
{
var container = (T)this.GetContainerForItem(item);
this.Remove(container, result);
}
return result;
}
protected override Control CreateContainerOverride(object item)
{
T result = item as T;
if (result == null)
{
TreeDataTemplate template = this.GetTreeDataTemplate(item);
result = new T
{
Header = template.Build(item),
Items = template.ItemsSelector(item),
IsExpanded = template.IsExpanded(item),
};
}
return result;
}
private TreeDataTemplate GetTreeDataTemplate(object item)
{
IDataTemplate template = this.Owner.FindDataTemplate(item);
if (template == null)
{
template = DataTemplate.Default;
}
TreeDataTemplate treeTemplate = template as TreeDataTemplate;
if (treeTemplate == null)
{
treeTemplate = new TreeDataTemplate(template.Build, x => null);
}
return treeTemplate;
}
private void Remove(T container, List<Control> removed)
{
if (container.Items != null)
{
foreach (var childItem in container.Items)
{
var childContainer = (T)this.GetContainerForItem(childItem);
if (childContainer != null)
{
this.Remove(childContainer, removed);
}
}
}
this.RemoveByContainerInternal(container);
removed.Add(container);
}
////IEnumerable<Control> IItemContainerGenerator.Remove(IEnumerable items)
////{
//// throw new NotImplementedException();
//// var result = new List<Control>();
//// foreach (var item in items)
//// {
//// var container = (T)this.GetContainerForItem(item);
//// this.Remove(container, result);
//// }
//// return result;
////}
////protected override Control CreateContainerOverride(object item)
////{
//// T result = item as T;
//// if (result == null)
//// {
//// TreeDataTemplate template = this.GetTreeDataTemplate(item);
//// result = new T
//// {
//// Header = template.Build(item),
//// Items = template.ItemsSelector(item),
//// IsExpanded = template.IsExpanded(item),
//// };
//// }
//// return result;
////}
////private TreeDataTemplate GetTreeDataTemplate(object item)
////{
//// IDataTemplate template = this.Owner.FindDataTemplate(item);
//// if (template == null)
//// {
//// template = DataTemplate.Default;
//// }
//// TreeDataTemplate treeTemplate = template as TreeDataTemplate;
//// if (treeTemplate == null)
//// {
//// treeTemplate = new TreeDataTemplate(template.Build, x => null);
//// }
//// return treeTemplate;
////}
////private void Remove(T container, List<Control> removed)
////{
//// if (container.Items != null)
//// {
//// foreach (var childItem in container.Items)
//// {
//// var childContainer = (T)this.GetContainerForItem(childItem);
//// if (childContainer != null)
//// {
//// this.Remove(childContainer, removed);
//// }
//// }
//// }
//// this.RemoveByContainerInternal(container);
//// removed.Add(container);
////}
}
}

2
Perspex.Controls/Generators/TypedItemContainerGenerator.cs

@ -15,7 +15,7 @@ namespace Perspex.Controls.Generators
{
}
protected override Control CreateContainerOverride(object item)
protected override IControl CreateContainer(object item, IDataTemplate itemTemplate)
{
T result = item as T;

22
Perspex.Controls/ItemsControl.cs

@ -77,29 +77,29 @@ namespace Perspex.Controls
set { this.SetValue(ItemsPanelProperty, value); }
}
IPerspexReadOnlyList<ILogical> ILogical.LogicalChildren
{
get
{
this.ApplyTemplate();
return this.logicalChildren;
}
}
protected IItemsPresenter Presenter
public IItemsPresenter Presenter
{
get
{
return this.presenter;
}
set
protected set
{
this.presenter = value;
this.logicalChildren.Source = ((IVisual)value?.Panel)?.VisualChildren;
}
}
IPerspexReadOnlyList<ILogical> ILogical.LogicalChildren
{
get
{
this.ApplyTemplate();
return this.logicalChildren;
}
}
protected virtual ItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator(this);

5
Perspex.Controls/Menu.cs

@ -63,8 +63,9 @@ namespace Perspex.Controls
{
get
{
return (this.SelectedItem != null) ?
(MenuItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem) :
var index = this.SelectedIndex;
return (index != -1) ?
(MenuItem)this.ItemContainerGenerator.ContainerFromIndex(index) :
null;
}
}

6
Perspex.Controls/MenuItem.cs

@ -476,11 +476,11 @@ namespace Perspex.Controls
/// <param name="e">The event args.</param>
private void PopupOpened(object sender, EventArgs e)
{
var selected = this.SelectedItem;
var selected = this.SelectedIndex;
if (selected != null)
if (selected != -1)
{
var container = this.ItemContainerGenerator.GetContainerForItem(selected);
var container = this.ItemContainerGenerator.ContainerFromIndex(selected);
if (container != null)
{

1
Perspex.Controls/Perspex.Controls.csproj

@ -36,6 +36,7 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="Generators\ItemContainers.cs" />
<Compile Include="IControl.cs" />
<Compile Include="ISetLogicalParent.cs" />
<Compile Include="MenuItemAccessKeyHandler.cs" />

20
Perspex.Controls/Presenters/DeckPresenter.cs

@ -102,7 +102,11 @@ namespace Perspex.Controls.Presenters
((IItemsPanel)this.Panel).ChildLogicalParent = this.TemplatedParent as ILogical;
this.AddVisualChild(this.Panel);
this.createdPanel = true;
this.SelectedItemChanged(Tuple.Create<object, object>(null, this.SelectedItem));
if (this.SelectedItem != null)
{
this.SelectedItemChanged(Tuple.Create<object, object>(null, this.SelectedItem));
}
}
private IItemContainerGenerator GetGenerator()
@ -121,37 +125,37 @@ namespace Perspex.Controls.Presenters
if (this.createdPanel)
{
var generator = this.GetGenerator();
Control from = null;
Control to = null;
IControl from = null;
IControl to = null;
int fromIndex = -1;
int toIndex = -1;
if (value.Item1 != null)
{
from = generator.GetContainerForItem(value.Item1);
fromIndex = this.Items.IndexOf(value.Item1);
from = generator.ContainerFromIndex(fromIndex);
}
if (value.Item2 != null)
{
to = generator.Generate(new[] { value.Item2 }).FirstOrDefault();
toIndex = this.Items.IndexOf(value.Item2);
to = generator.CreateContainers(toIndex, new[] { value.Item2 }, null).FirstOrDefault();
if (to != null)
{
this.Panel.Children.Add(to);
toIndex = this.Items.IndexOf(value.Item2);
}
}
if (this.Transition != null)
{
await this.Transition.Start(from, to, fromIndex < toIndex);
await this.Transition.Start((Visual)from, (Visual)to, fromIndex < toIndex);
}
if (from != null)
{
this.Panel.Children.Remove(from);
generator.Remove(new[] { value.Item1 });
generator.RemoveContainers(fromIndex, 1);
}
}
}

11
Perspex.Controls/Presenters/ItemsPresenter.cs

@ -147,7 +147,8 @@ namespace Perspex.Controls.Presenters
{
if (items != null)
{
this.Panel.Children.AddRange(this.ItemContainerGenerator.Generate(this.Items));
this.Panel.Children.AddRange(
this.ItemContainerGenerator.CreateContainers(0, this.Items, null));
INotifyCollectionChanged incc = items as INotifyCollectionChanged;
@ -170,7 +171,7 @@ namespace Perspex.Controls.Presenters
if (e.OldValue != null)
{
generator.RemoveAll();
generator.ClearContainers();
this.Panel.Children.Clear();
INotifyCollectionChanged incc = e.OldValue as INotifyCollectionChanged;
@ -203,11 +204,13 @@ namespace Perspex.Controls.Presenters
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
this.Panel.Children.AddRange(generator.Generate(e.NewItems));
this.Panel.Children.AddRange(
generator.CreateContainers(e.NewStartingIndex, e.NewItems, null));
break;
case NotifyCollectionChangedAction.Remove:
this.Panel.Children.RemoveAll(generator.Remove(e.OldItems));
this.Panel.Children.RemoveAll(
generator.RemoveContainers(e.OldStartingIndex, e.OldItems.Count));
break;
}

343
Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -10,9 +10,10 @@ namespace Perspex.Controls.Primitives
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Perspex.Controls.Utils;
using Perspex.Controls.Generators;
using Perspex.Input;
using Perspex.Interactivity;
using Perspex.Styling;
using Perspex.VisualTree;
/// <summary>
@ -53,9 +54,17 @@ namespace Perspex.Controls.Primitives
/// </summary>
static SelectingItemsControl()
{
IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>(x => x.ItemIsSelectedChanged);
SelectedIndexProperty.Changed.Subscribe(SelectedIndexChanged);
SelectedItemProperty.Changed.Subscribe(SelectedItemChanged);
IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>(x => x.ContainerSelectionChanged);
SelectedIndexProperty.Changed.AddClassHandler<SelectingItemsControl>(x => x.SelectedIndexChanged);
SelectedItemProperty.Changed.AddClassHandler<SelectingItemsControl>(x => x.SelectedItemChanged);
}
/// <summary>
/// Initializes a new instance of the <see cref="SelectingItemsControl"/> class.
/// </summary>
public SelectingItemsControl()
{
this.ItemContainerGenerator.ContainersInitialized.Subscribe(this.ContainersInitialized);
}
/// <summary>
@ -76,258 +85,186 @@ namespace Perspex.Controls.Primitives
set { this.SetValue(SelectedItemProperty, value); }
}
/// <summary>
/// Called when the <see cref="Items"/> property changes.
/// </summary>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
/// <inheritdoc/>
protected override void ItemsChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.ItemsChanged(oldValue, newValue);
var selected = this.SelectedItem;
if (selected != null)
{
if (newValue == null || !newValue.Contains(selected))
{
this.SelectedItem = null;
}
}
this.SelectedIndex = IndexOf(newValue, this.SelectedItem);
}
/// <summary>
/// Called when a <see cref="INotifyCollectionChanged.CollectionChanged"/> event is raised
/// on <see cref="Items"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
/// <inheritdoc/>
protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
base.ItemsCollectionChanged(sender, e);
var selected = this.SelectedItem;
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
if (e.OldItems.Contains(selected))
case NotifyCollectionChangedAction.Replace:
var selectedIndex = this.SelectedIndex;
if (selectedIndex >= e.OldStartingIndex &&
selectedIndex < e.OldStartingIndex + e.OldItems.Count)
{
this.SelectedItem = null;
this.SelectedIndex = -1;
}
break;
case NotifyCollectionChangedAction.Move:
this.SelectedItem = this.Items.IndexOf(selected);
case NotifyCollectionChangedAction.Reset:
this.SelectedIndex = IndexOf(e.NewItems, this.SelectedItem);
break;
}
}
/// <summary>
/// Called when the selection on a child item changes.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void ItemIsSelectedChanged(RoutedEventArgs e)
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
var selectable = e.Source as ISelectable;
if (selectable != null && selectable != this && selectable.IsSelected)
{
var container = this.ItemContainerGenerator.GetItemForContainer((Control)selectable);
if (container != null)
{
this.SelectedItem = container;
e.Handled = true;
}
}
base.OnGotFocus(e);
this.TrySetSelectionFromContainerEvent(e.Source);
}
/// <summary>
/// Moves the selection in the specified direction.
/// Gets the index of an item in a collection.
/// </summary>
/// <param name="direction">The direction.</param>
protected virtual void MoveSelection(FocusNavigationDirection direction)
/// <param name="items">The collection.</param>
/// <param name="item">The item.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
private static int IndexOf(IEnumerable items, object item)
{
var panel = this.Presenter?.Panel as INavigablePanel;
var selected = this.SelectedItem;
var container = selected != null ?
this.ItemContainerGenerator.GetContainerForItem(selected) :
null;
if (panel != null)
if (items != null && item != null)
{
var next = panel.GetControl(direction, container);
if (next != null)
{
this.SelectedItem = this.ItemContainerGenerator.GetItemForContainer((Control)next);
}
}
else
{
// TODO: Try doing a visual search?
}
}
var list = items as IList;
/// <summary>
/// Called when a key is pressed within the control.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled)
{
switch (e.Key)
if (list != null)
{
case Key.Up:
this.MoveSelection(FocusNavigationDirection.Up);
break;
case Key.Down:
this.MoveSelection(FocusNavigationDirection.Down);
break;
case Key.Left:
this.MoveSelection(FocusNavigationDirection.Left);
break;
case Key.Right:
this.MoveSelection(FocusNavigationDirection.Right);
break;
default:
return;
return list.IndexOf(item);
}
var selected = this.SelectedItem;
if (selected != null)
else
{
var container = this.ItemContainerGenerator.GetContainerForItem(selected);
int index = 0;
if (container != null)
foreach (var i in items)
{
container.BringIntoView();
FocusManager.Instance.Focus(container, true);
if (object.Equals(i, item))
{
return index;
}
++index;
}
}
e.Handled = true;
}
return -1;
}
/// <summary>
/// Called when the pointer is pressed within the control.
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerPressed(PointerPressEventArgs e)
/// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param>
private static void MarkContainerSelected(IControl container, bool selected)
{
IVisual source = (IVisual)e.Source;
var selectable = source.GetVisualAncestors()
.OfType<ISelectable>()
.OfType<Control>()
.FirstOrDefault();
var selectable = container as ISelectable;
var styleable = container as IStyleable;
if (selectable != null)
{
var item = this.ItemContainerGenerator.GetItemForContainer(selectable);
if (item != null)
selectable.IsSelected = selected;
}
else if (styleable != null)
{
if (selected)
{
this.SelectedItem = item;
selectable.BringIntoView();
FocusManager.Instance.Focus(selectable);
styleable.Classes.Add("selected");
}
else
{
styleable.Classes.Remove("selected");
}
}
e.Handled = true;
}
/// <summary>
/// Called when the control's template has been applied.
/// Coerces the <see cref="SelectedIndex"/> property.
/// </summary>
protected override void OnTemplateApplied()
/// <param name="sender">The object with the property.</param>
/// <param name="index">The proposed value of the property.</param>
/// <returns>The final value of the property.</returns>
private static int ValidateSelectedIndex(SelectingItemsControl sender, int index)
{
base.OnTemplateApplied();
this.SelectedItemChanged(this.SelectedItem);
var items = sender.Items;
return (index >= 0 && index < items?.Cast<object>().Count()) ? index : -1;
}
/// <summary>
/// Provides coercion for the <see cref="SelectedIndex"/> property.
/// Coerces the <see cref="SelectedItem"/> property.
/// </summary>
/// <param name="o">The object on which the property has changed.</param>
/// <param name="value">The proposed value.</param>
/// <returns>The coerced value.</returns>
private static int ValidateSelectedIndex(PerspexObject o, int value)
/// <param name="sender">The object with the property.</param>
/// <param name="item">The proposed value of the property.</param>
/// <returns>The final value of the property.</returns>
private static object ValidateSelectedItem(SelectingItemsControl sender, object item)
{
var control = o as SelectingItemsControl;
if (control != null)
{
if (value < -1)
{
return -1;
}
else if (value > -1)
{
var items = control.Items;
if (items != null)
{
var count = items.Count();
return Math.Min(value, count - 1);
}
else
{
return -1;
}
}
}
return value;
var items = sender.Items;
return items?.Cast<object>().Contains(item) == true ? item : null;
}
/// <summary>
/// Provides coercion for the <see cref="SelectedItem"/> property.
/// Called when new containers are initialized by the <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="o">The object on which the property has changed.</param>
/// <param name="value">The proposed value.</param>
/// <returns>The coerced value.</returns>
private static object ValidateSelectedItem(PerspexObject o, object value)
/// <param name="containers">The containers.</param>
private void ContainersInitialized(ItemContainers containers)
{
var control = o as SelectingItemsControl;
var selectedIndex = this.SelectedIndex;
var selectedContainer = containers.Items.OfType<ISelectable>().FirstOrDefault(x => x.IsSelected);
if (control != null)
if (selectedContainer != null)
{
if (value != null && (control.Items == null || control.Items.IndexOf(value) == -1))
{
return null;
}
this.SelectedIndex = containers.Items.IndexOf((IControl)selectedContainer) + containers.StartingIndex;
}
else if (selectedIndex >= containers.StartingIndex &&
selectedIndex < containers.StartingIndex + containers.Items.Count)
{
var container = containers.Items[selectedIndex - containers.StartingIndex];
MarkContainerSelected(container, true);
}
}
return value;
/// <summary>
/// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
/// </summary>
/// <param name="e">The event.</param>
private void ContainerSelectionChanged(RoutedEventArgs e)
{
this.TrySetSelectionFromContainerEvent(e.Source);
}
/// <summary>
/// Called when the <see cref="SelectedIndex"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void SelectedIndexChanged(PerspexPropertyChangedEventArgs e)
private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e)
{
var control = e.Sender as SelectingItemsControl;
var index = (int)e.OldValue;
if (control != null)
if (index != -1)
{
var index = (int)e.NewValue;
var container = this.ItemContainerGenerator.ContainerFromIndex(index);
MarkContainerSelected(container, false);
}
if (index == -1)
{
control.SelectedItem = null;
}
else
{
control.SelectedItem = control.Items.ElementAt((int)e.NewValue);
}
index = (int)e.NewValue;
if (index == -1)
{
this.SelectedItem = null;
}
else
{
this.SelectedItem = this.Items.Cast<object>().ElementAt((int)e.NewValue);
var container = this.ItemContainerGenerator.ContainerFromIndex(index);
MarkContainerSelected(container, true);
}
}
@ -335,50 +272,28 @@ namespace Perspex.Controls.Primitives
/// Called when the <see cref="SelectedItem"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void SelectedItemChanged(PerspexPropertyChangedEventArgs e)
private void SelectedItemChanged(PerspexPropertyChangedEventArgs e)
{
var control = e.Sender as SelectingItemsControl;
if (control != null)
{
control.SelectedItemChanged(e.NewValue);
}
this.SelectedIndex = IndexOf(this.Items, e.NewValue);
}
/// <summary>
/// Called when the <see cref="SelectedItem"/> property changes.
/// Tries to set the selection to a container that raised an event.
/// </summary>
/// <param name="selected">The new selected item.</param>
private void SelectedItemChanged(object selected)
/// <param name="eventSource">The control that raised the event.</param>
private void TrySetSelectionFromContainerEvent(IInteractive eventSource)
{
var containers = this.ItemContainerGenerator.GetAll()
.Select(x => x.Item2)
.OfType<ISelectable>();
var selectedContainer = (selected != null) ?
this.ItemContainerGenerator.GetContainerForItem(selected) :
null;
if (this.Presenter != null && this.Presenter.Panel != null)
{
KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer);
}
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
.OfType<ILogical>()
.FirstOrDefault(x => x.LogicalParent == this);
foreach (var item in containers)
{
item.IsSelected = item == selectedContainer;
}
if (selected == null)
{
this.SelectedIndex = -1;
}
else
if (item != null)
{
var items = this.Items;
var index = this.ItemContainerGenerator.IndexFromContainer((IControl)item);
if (items != null)
if (index != -1)
{
this.SelectedIndex = items.IndexOf(selected);
this.SelectedIndex = index;
}
}
}

18
Perspex.Controls/Primitives/TabStrip.cs

@ -47,22 +47,22 @@ namespace Perspex.Controls.Primitives
result = new TypedItemContainerGenerator<TabItem>(this);
}
result.StateChanged += this.ItemsContainerGeneratorStateChanged;
////result.StateChanged += this.ItemsContainerGeneratorStateChanged;
return result;
}
private void ItemsContainerGeneratorStateChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.State == ItemContainerGeneratorState.Generated)
{
var tabs = this.ItemContainerGenerator.GetAll()
.Select(x => x.Item2)
.OfType<TabItem>()
.ToList();
////if (this.ItemContainerGenerator.State == ItemContainerGeneratorState.Generated)
////{
//// var tabs = this.ItemContainerGenerator.GetAll()
//// .Select(x => x.Item2)
//// .OfType<TabItem>()
//// .ToList();
this.SelectedItem = tabs.FirstOrDefault(x => x.IsSelected) ?? tabs.FirstOrDefault();
}
//// this.SelectedItem = tabs.FirstOrDefault(x => x.IsSelected) ?? tabs.FirstOrDefault();
////}
}
}
}

27
Perspex.Controls/Templates/DataTemplateExtensions.cs

@ -1,6 +1,6 @@
// -----------------------------------------------------------------------
// <copyright file="DataTemplateExtensions.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
@ -10,9 +10,18 @@ namespace Perspex.Controls.Templates
using Perspex.LogicalTree;
using Splat;
/// <summary>
/// Defines extension methods for working with <see cref="IDataTemplate"/>s.
/// </summary>
public static class DataTemplateExtensions
{
public static IControl MaterializeDataTemplate(this Control control, object data)
/// <summary>
/// Materializes a piece of data based on a data template.
/// </summary>
/// <param name="control">The control materializing the data template.</param>
/// <param name="data">The data.</param>
/// <returns>The data materialized as a control.</returns>
public static IControl MaterializeDataTemplate(this IControl control, object data)
{
IDataTemplate template = control.FindDataTemplate(data);
IControl result;
@ -26,9 +35,9 @@ namespace Perspex.Controls.Templates
result.DataContext = data;
}
}
else if (data is Control)
else if (data is IControl)
{
result = (Control)data;
result = (IControl)data;
}
else
{
@ -38,9 +47,15 @@ namespace Perspex.Controls.Templates
return result;
}
public static IDataTemplate FindDataTemplate(this Control control, object data)
/// <summary>
/// Find a data template that matches a piece of data.
/// </summary>
/// <param name="control">The control searching for the data template.</param>
/// <param name="data">The data.</param>
/// <returns>The data template or null if no matching data template was found.</returns>
public static IDataTemplate FindDataTemplate(this IControl control, object data)
{
foreach (var i in control.GetSelfAndLogicalAncestors().OfType<Control>())
foreach (var i in control.GetSelfAndLogicalAncestors().OfType<IControl>())
{
foreach (IDataTemplate dt in i.DataTemplates.Reverse())
{

260
Perspex.Controls/TreeView.cs

@ -47,115 +47,115 @@ namespace Perspex.Controls
{
// TODO: Up and down movement is a *HACK* and probably pretty slow. Probably needs
// rewriting at some point.
if (this.SelectedItem != null)
{
switch (direction)
{
case FocusNavigationDirection.Up:
{
var list = this.Flatten();
var index = list.IndexOf(this.SelectedItem);
if (index > 0)
{
this.SelectedItem = list[index - 1];
}
break;
}
case FocusNavigationDirection.Down:
{
var list = this.Flatten();
var index = list.IndexOf(this.SelectedItem);
if (index + 1 < list.Count)
{
this.SelectedItem = list[index + 1];
}
break;
}
case FocusNavigationDirection.Left:
{
var node = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem);
node.IsExpanded = false;
break;
}
case FocusNavigationDirection.Right:
{
var node = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem);
node.IsExpanded = true;
break;
}
}
}
////if (this.SelectedItem != null)
////{
//// switch (direction)
//// {
//// case FocusNavigationDirection.Up:
//// {
//// var list = this.Flatten();
//// var index = list.IndexOf(this.SelectedItem);
//// if (index > 0)
//// {
//// this.SelectedItem = list[index - 1];
//// }
//// break;
//// }
//// case FocusNavigationDirection.Down:
//// {
//// var list = this.Flatten();
//// var index = list.IndexOf(this.SelectedItem);
//// if (index + 1 < list.Count)
//// {
//// this.SelectedItem = list[index + 1];
//// }
//// break;
//// }
//// case FocusNavigationDirection.Left:
//// {
//// var node = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem);
//// node.IsExpanded = false;
//// break;
//// }
//// case FocusNavigationDirection.Right:
//// {
//// var node = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem);
//// node.IsExpanded = true;
//// break;
//// }
//// }
////}
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled)
{
switch (e.Key)
{
case Key.Up:
this.MoveSelection(FocusNavigationDirection.Up);
break;
case Key.Down:
this.MoveSelection(FocusNavigationDirection.Down);
break;
case Key.Left:
this.MoveSelection(FocusNavigationDirection.Left);
break;
case Key.Right:
this.MoveSelection(FocusNavigationDirection.Right);
break;
default:
return;
}
var selected = this.SelectedItem;
if (selected != null)
{
var container = this.ItemContainerGenerator.GetContainerForItem(selected);
if (container != null)
{
container.BringIntoView();
FocusManager.Instance.Focus(container, true);
}
}
e.Handled = true;
}
////base.OnKeyDown(e);
////if (!e.Handled)
////{
//// switch (e.Key)
//// {
//// case Key.Up:
//// this.MoveSelection(FocusNavigationDirection.Up);
//// break;
//// case Key.Down:
//// this.MoveSelection(FocusNavigationDirection.Down);
//// break;
//// case Key.Left:
//// this.MoveSelection(FocusNavigationDirection.Left);
//// break;
//// case Key.Right:
//// this.MoveSelection(FocusNavigationDirection.Right);
//// break;
//// default:
//// return;
//// }
//// var selected = this.SelectedItem;
//// if (selected != null)
//// {
//// var container = this.ItemContainerGenerator.GetContainerForItem(selected);
//// if (container != null)
//// {
//// container.BringIntoView();
//// FocusManager.Instance.Focus(container, true);
//// }
//// }
//// e.Handled = true;
////}
}
protected override void OnPointerPressed(PointerPressEventArgs e)
{
IVisual source = (IVisual)e.Source;
var selectable = source.GetVisualAncestors()
.OfType<ISelectable>()
.OfType<Control>()
.FirstOrDefault();
if (selectable != null)
{
var item = this.ItemContainerGenerator.GetItemForContainer(selectable);
if (item != null)
{
this.SelectedItem = item;
selectable.BringIntoView();
FocusManager.Instance.Focus(selectable);
}
}
e.Handled = true;
////IVisual source = (IVisual)e.Source;
////var selectable = source.GetVisualAncestors()
//// .OfType<ISelectable>()
//// .OfType<Control>()
//// .FirstOrDefault();
////if (selectable != null)
////{
//// var item = this.ItemContainerGenerator.GetItemForContainer(selectable);
//// if (item != null)
//// {
//// this.SelectedItem = item;
//// selectable.BringIntoView();
//// FocusManager.Instance.Focus(selectable);
//// }
////}
////e.Handled = true;
}
private static object ValidateSelectedItem(PerspexObject o, object value)
@ -182,39 +182,39 @@ namespace Perspex.Controls
private void Flatten(IEnumerable items, List<object> result)
{
if (items != null)
{
foreach (object item in items)
{
var container = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(item);
result.Add(item);
if (container.IsExpanded)
{
this.Flatten(container.Items, result);
}
}
}
////if (items != null)
////{
//// foreach (object item in items)
//// {
//// var container = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(item);
//// result.Add(item);
//// if (container.IsExpanded)
//// {
//// this.Flatten(container.Items, result);
//// }
//// }
////}
}
private void SelectedItemChanged(object selected)
{
var containers = this.ItemContainerGenerator.GetAll()
.Select(x => x.Item2)
.OfType<ISelectable>();
var selectedContainer = (selected != null) ?
this.ItemContainerGenerator.GetContainerForItem(selected) :
null;
if (this.Presenter != null && this.Presenter.Panel != null)
{
KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer);
}
foreach (var item in containers)
{
item.IsSelected = item == selectedContainer;
}
////var containers = this.ItemContainerGenerator.GetAll()
//// .Select(x => x.Item2)
//// .OfType<ISelectable>();
////var selectedContainer = (selected != null) ?
//// this.ItemContainerGenerator.GetContainerForItem(selected) :
//// null;
////if (this.Presenter != null && this.Presenter.Panel != null)
////{
//// KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer);
////}
////foreach (var item in containers)
////{
//// item.IsSelected = item == selectedContainer;
////}
}
}
}

6
Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs

@ -91,10 +91,14 @@ namespace Perspex.Controls.UnitTests.Presenters
};
target.ApplyTemplate();
items.RemoveAt(2);
var text = target.Panel.Children.OfType<TextBlock>().Select(x => x.Text).ToList();
Assert.Equal(new[] { "foo", "bar" }, text);
items.RemoveAt(1);
text = target.Panel.Children.OfType<TextBlock>().Select(x => x.Text).ToList();
Assert.Equal(new[] { "foo", "bar" }, text);
}
[Fact]

74
Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -6,6 +6,7 @@
namespace Perspex.Controls.UnitTests.Primitives
{
using System.Collections.ObjectModel;
using Perspex.Collections;
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
@ -192,7 +193,7 @@ namespace Perspex.Controls.UnitTests.Primitives
}
[Fact]
public void Setting_SelectedIndex_Should_Coerce()
public void Setting_SelectedIndex_Out_Of_Bounds_Should_Clear_Selection()
{
var items = new[]
{
@ -209,11 +210,11 @@ namespace Perspex.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.SelectedIndex = 2;
Assert.Equal(1, target.SelectedIndex);
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Setting_SelectedIndex_With_No_Items_Should_Not_Throw_Exception()
public void Setting_SelectedItem_To_Non_Existent_Item_Should_Clear_Selection()
{
var target = new Target
{
@ -221,25 +222,36 @@ namespace Perspex.Controls.UnitTests.Primitives
};
target.ApplyTemplate();
target.SelectedIndex = 2;
target.SelectedItem = new Item();
Assert.Equal(-1, target.SelectedIndex);
Assert.Null(target.SelectedItem);
}
[Fact]
public void Setting_SelectedItem_With_No_Items_Should_Not_Throw_Exception()
public void Adding_Selected_Item_Should_Update_Selection()
{
var items = new PerspexList<Item>(new[]
{
new Item(),
new Item(),
});
var target = new Target
{
Items = items,
Template = this.Template(),
};
target.ApplyTemplate();
target.SelectedItem = new Item();
items.Add(new Item { IsSelected = true });
Assert.Equal(2, target.SelectedIndex);
Assert.Equal(items[2], target.SelectedItem);
}
[Fact]
public void Clearing_Items_Should_Clear_Selection()
public void Setting_Items_To_Null_Should_Clear_Selection()
{
var items = new PerspexList<Item>
{
@ -293,18 +305,54 @@ namespace Perspex.Controls.UnitTests.Primitives
}
[Fact]
public void PointerPressed_Event_Should_Be_Handled()
public void Resetting_Items_Collection_Should_Clear_Selection()
{
var target = new Target();
// Need to use ObservableCollection here as PerspexList signals a Clear as an
// add + remove.
var items = new ObservableCollection<Item>
{
new Item(),
new Item(),
};
var e = new PointerPressEventArgs
var target = new Target
{
RoutedEvent = InputElement.PointerPressedEvent
Items = items,
Template = this.Template(),
};
target.RaiseEvent(e);
target.ApplyTemplate();
target.SelectedIndex = 1;
Assert.True(e.Handled);
Assert.Equal(items[1], target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
items.Clear();
Assert.Equal(null, target.SelectedItem);
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Focusing_Item_Should_Select_It()
{
var target = new Target
{
Template = this.Template(),
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
var e = new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent
};
target.Presenter.Panel.Children[1].RaiseEvent(e);
Assert.Equal(1, target.SelectedIndex);
Assert.False(e.Handled);
}
[Fact]

Loading…
Cancel
Save