Browse Source

Started implementing multiple selection.

pull/278/head
Steven Kirk 11 years ago
parent
commit
b2d40e77c3
  1. 2
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  2. 82
      src/Perspex.Base/Collections/PerspexListExtensions.cs
  3. 5
      src/Perspex.Controls/Generators/IItemContainerGenerator.cs
  4. 5
      src/Perspex.Controls/Generators/ItemContainerGenerator.cs
  5. 5
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  6. 36
      src/Perspex.Controls/ListBox.cs
  7. 332
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  8. 23
      src/Perspex.Controls/Primitives/TabStrip.cs
  9. 19
      tests/Perspex.Controls.UnitTests/ListBoxTests.cs
  10. 188
      tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs
  11. 2
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  12. 95
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  13. 246
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

2
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -49,7 +49,7 @@
</TabItem>
<TabItem Header="Lists">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<ListBox Items="{Binding Items}">
<ListBox Items="{Binding Items}" SelectionMode="Toggle">
<ListBox.DataTemplates>
<DataTemplate DataType="vm:TestItem">
<StackPanel>

82
src/Perspex.Base/Collections/PerspexListExtensions.cs

@ -22,55 +22,110 @@ namespace Perspex.Collections
/// <param name="collection">The collection.</param>
/// <param name="added">
/// An action called initially for each item in the collection and subsequently for each
/// item added to the collection.
/// item added to the collection. The parameters passed are the index in the collection and
/// the item.
/// </param>
/// <param name="removed">
/// An action called for each item removed from the collection.
/// An action called for each item removed from the collection. The parameters passed are
/// the index in the collection and the item.
/// </param>
/// <param name="reset">
/// An action called when the collection is reset.
/// </param>
/// <returns>A disposable used to terminate the subscription.</returns>
public static IDisposable ForEachItem<T>(
this IPerspexReadOnlyList<T> collection,
Action<T> added,
Action<T> removed)
Action<T> removed,
Action reset)
{
return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset);
}
/// <summary>
/// Invokes an action for each item in a collection and subsequently each item added or
/// removed from the collection.
/// </summary>
/// <typeparam name="T">The type of the collection items.</typeparam>
/// <param name="collection">The collection.</param>
/// <param name="added">
/// An action called initially for each item in the collection and subsequently for each
/// item added to the collection. The parameters passed are the index in the collection and
/// the item.
/// </param>
/// <param name="removed">
/// An action called for each item removed from the collection. The parameters passed are
/// the index in the collection and the item.
/// </param>
/// <param name="reset">
/// An action called when the collection is reset.
/// </param>
/// <returns>A disposable used to terminate the subscription.</returns>
public static IDisposable ForEachItem<T>(
this IPerspexReadOnlyList<T> collection,
Action<int, T> added,
Action<int, T> removed,
Action reset)
{
int index;
NotifyCollectionChangedEventHandler handler = (_, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (T i in e.NewItems)
index = e.NewStartingIndex;
foreach (T item in e.NewItems)
{
added(i);
added(index++, item);
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (T i in e.OldItems)
index = e.OldStartingIndex;
foreach (T item in e.OldItems)
{
removed(i);
removed(index++, item);
}
foreach (T i in e.NewItems)
index = e.NewStartingIndex;
foreach (T item in e.NewItems)
{
added(i);
added(index++, item);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (T i in e.OldItems)
index = e.OldStartingIndex;
foreach (T item in e.OldItems)
{
removed(i);
removed(index++, item);
}
break;
case NotifyCollectionChangedAction.Reset:
if (reset == null)
{
throw new InvalidOperationException(
"Reset called on collection without reset handler.");
}
reset();
break;
}
};
index = 0;
foreach (T i in collection)
{
added(i);
added(index++, i);
}
collection.CollectionChanged += handler;
@ -116,7 +171,8 @@ namespace Perspex.Collections
inpc.PropertyChanged -= handler;
tracked.Remove(inpc);
}
});
},
null);
return Disposable.Create(() =>
{

5
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

@ -13,6 +13,11 @@ namespace Perspex.Controls.Generators
/// </summary>
public interface IItemContainerGenerator
{
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<IControl> Containers { get; }
/// <summary>
/// Signalled whenever new containers are initialized.
/// </summary>

5
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

@ -28,6 +28,11 @@ namespace Perspex.Controls.Generators
Owner = owner;
}
/// <summary>
/// Gets the currently realized containers.
/// </summary>
public IEnumerable<IControl> Containers => _containers.Values;
/// <summary>
/// Signalled whenever new containers are initialized.
/// </summary>

5
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@ -29,6 +29,11 @@ namespace Perspex.Controls.Generators
Owner = owner;
}
/// <summary>
/// Gets the currently realized containers.
/// </summary>
public IEnumerable<IControl> Containers => _containers.Values;
/// <summary>
/// Signalled whenever new containers are initialized.
/// </summary>

36
src/Perspex.Controls/ListBox.cs

@ -3,6 +3,7 @@
using Perspex.Controls.Generators;
using Perspex.Controls.Primitives;
using Perspex.Input;
namespace Perspex.Controls
{
@ -11,10 +12,45 @@ namespace Perspex.Controls
/// </summary>
public class ListBox : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
public static readonly new PerspexProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
/// <inheritdoc/>
public new SelectionMode SelectionMode
{
get { return base.SelectionMode; }
set { base.SelectionMode = value; }
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ListBoxItem>(this);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (e.NavigationMethod == NavigationMethod.Directional)
{
UpdateSelectionFromEventSource(e.Source, true);
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressEventArgs e)
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
{
UpdateSelectionFromEventSource(e.Source, true);
}
}
}
}

332
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -3,7 +3,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Perspex.Collections;
@ -18,6 +17,22 @@ namespace Perspex.Controls.Primitives
/// <summary>
/// An <see cref="ItemsControl"/> that maintains a selection.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="SelectingItemsControl"/> provides a base class for <see cref="ItemsControl"/>s
/// that maintain a selection (single or multiple). By default only its
/// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the
/// multiple selection properties <see cref="SelectedIndexes"/> and <see cref="SelectedItems"/>
/// together with the <see cref="SelectionMode"/> properties are protected, however a derived
/// class can expose these if it wishes to support multiple selection.
/// </para>
/// <para>
/// <see cref="SelectingItemsControl"/> maintains a selection respecting the current
/// <see cref="SelectionMode"/> but it does not react to user input; this must be handled in a
/// derived class. It does, however, respond to <see cref="IsSelectedChangedEvent"/> events
/// from items and updates the selection accordingly.
/// </para>
/// </remarks>
public class SelectingItemsControl : ItemsControl
{
/// <summary>
@ -41,16 +56,16 @@ namespace Perspex.Controls.Primitives
/// <summary>
/// Defines the <see cref="SelectedIndexes"/> property.
/// </summary>
protected static readonly PerspexProperty<IList<int>> SelectedIndexesProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IList<int>>(
protected static readonly PerspexProperty<IPerspexList<int>> SelectedIndexesProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IPerspexList<int>>(
nameof(SelectedIndexes),
o => o.SelectedIndexes);
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
/// </summary>
protected static readonly PerspexProperty<IList<object>> SelectedItemsProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IList<object>>(
protected static readonly PerspexProperty<IPerspexList<object>> SelectedItemsProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IPerspexList<object>>(
nameof(SelectedItems),
o => o.SelectedItems);
@ -71,6 +86,7 @@ namespace Perspex.Controls.Primitives
private PerspexList<int> _selectedIndexes = new PerspexList<int>();
private PerspexList<object> _selectedItems = new PerspexList<object>();
private bool _ignoreContainerSelectionChanged;
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@ -78,8 +94,6 @@ namespace Perspex.Controls.Primitives
static SelectingItemsControl()
{
IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>(x => x.ContainerSelectionChanged);
SelectedIndexProperty.Changed.AddClassHandler<SelectingItemsControl>(x => x.SelectedIndexChanged);
SelectedItemProperty.Changed.AddClassHandler<SelectingItemsControl>(x => x.SelectedItemChanged);
}
/// <summary>
@ -88,6 +102,9 @@ namespace Perspex.Controls.Primitives
public SelectingItemsControl()
{
ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized);
_selectedIndexes.Validate = ValidateIndex;
_selectedIndexes.ForEachItem(SelectedIndexAdded, SelectedIndexRemoved, SelectionReset);
_selectedItems.ForEachItem(SelectedItemAdded, SelectedItemRemoved, SelectionReset);
}
/// <summary>
@ -151,7 +168,7 @@ namespace Perspex.Controls.Primitives
/// <summary>
/// Gets the selected indexes.
/// </summary>
protected IList<int> SelectedIndexes
protected IPerspexList<int> SelectedIndexes
{
get { return _selectedIndexes; }
}
@ -159,7 +176,7 @@ namespace Perspex.Controls.Primitives
/// <summary>
/// Gets the selected items.
/// </summary>
protected IList<object> SelectedItems
protected IPerspexList<object> SelectedItems
{
get { return _selectedItems; }
}
@ -178,6 +195,20 @@ namespace Perspex.Controls.Primitives
/// </summary>
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
/// <summary>
/// Tries to get the container that was the source of an event.
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <returns>The container or null if the event did not originate in a container.</returns>
protected IControl GetContainerFromEventSource(IInteractive eventSource)
{
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
.OfType<ILogical>()
.FirstOrDefault(x => x.LogicalParent == this);
return item as IControl;
}
/// <inheritdoc/>
protected override void ItemsChanged(PerspexPropertyChangedEventArgs e)
{
@ -233,23 +264,80 @@ namespace Perspex.Controls.Primitives
}
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
/// <summary>
/// Updates the selection for an item.
/// </summary>
/// <param name="index">The index of the item.</param>
/// <param name="select">Whether the item should be selected or unselected.</param>
protected void UpdateSelection(int index, bool select)
{
if (index != -1)
{
if (select)
{
var toggle = (SelectionMode & SelectionMode.Toggle) != 0;
if (!toggle)
{
SelectedIndex = index;
}
else
{
var i = SelectedIndexes.IndexOf(index);
if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
{
SelectedIndexes.RemoveAt(i);
}
else
{
SelectedIndexes.Add(index);
}
}
}
else
{
LostSelection();
}
}
}
/// <summary>
/// Updates the selection for a container.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="select">Whether the container should be selected or unselected.</param>
protected void UpdateSelection(IControl container, bool select)
{
base.OnGotFocus(e);
var index = ItemContainerGenerator.IndexFromContainer(container);
if (e.NavigationMethod == NavigationMethod.Pointer ||
e.NavigationMethod == NavigationMethod.Directional)
if (index != -1)
{
TrySetSelectionFromContainerEvent(e.Source, true);
UpdateSelection(index, select);
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressEventArgs e)
/// <summary>
/// Updates the selection based on an event source that may have originated in a container
/// that belongs to the control.
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <param name="select">Whether the container should be selected or unselected.</param>
/// <returns>
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
/// </returns>
protected bool UpdateSelectionFromEventSource(IInteractive eventSource, bool select)
{
base.OnPointerPressed(e);
e.Handled = true;
var item = GetContainerFromEventSource(eventSource);
if (item != null)
{
UpdateSelection(item, select);
return true;
}
return false;
}
/// <summary>
@ -292,26 +380,35 @@ namespace Perspex.Controls.Primitives
/// </summary>
/// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param>
private static void MarkContainerSelected(IControl container, bool selected)
private void MarkContainerSelected(IControl container, bool selected)
{
var selectable = container as ISelectable;
var styleable = container as IStyleable;
if (selectable != null)
{
selectable.IsSelected = selected;
}
else if (styleable != null)
try
{
if (selected)
var selectable = container as ISelectable;
var styleable = container as IStyleable;
_ignoreContainerSelectionChanged = true;
if (selectable != null)
{
styleable.Classes.Add(":selected");
selectable.IsSelected = selected;
}
else
else if (styleable != null)
{
styleable.Classes.Remove(":selected");
if (selected)
{
styleable.Classes.Add(":selected");
}
else
{
styleable.Classes.Remove(":selected");
}
}
}
finally
{
_ignoreContainerSelectionChanged = false;
}
}
/// <summary>
@ -341,71 +438,143 @@ namespace Perspex.Controls.Primitives
/// <param name="e">The event.</param>
private void ContainerSelectionChanged(RoutedEventArgs e)
{
var selectable = (ISelectable)e.Source;
if (!_ignoreContainerSelectionChanged)
{
var selectable = (ISelectable)e.Source;
if (selectable != null)
if (selectable != null)
{
UpdateSelectionFromEventSource(e.Source, selectable.IsSelected);
}
}
}
/// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="index">The index of the item.</param>
/// <param name="selected">Whether the control is selected</param>
/// <returns>The container.</returns>
private IControl MarkIndexSelected(int index, bool selected)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
if (container != null)
{
TrySetSelectionFromContainerEvent(e.Source, selectable.IsSelected);
MarkContainerSelected(container, selected);
}
return container;
}
/// <summary>
/// Called when the <see cref="SelectedIndex"/> property changes.
/// Called when an index is added to the <see cref="SelectedIndexes"/> collection.
/// </summary>
/// <param name="e">The event args.</param>
private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e)
/// <param name="listIndex">The index in the SelectedIndexes collection.</param>
/// <param name="itemIndex">The item index.</param>
private void SelectedIndexAdded(int listIndex, int itemIndex)
{
var index = (int)e.OldValue;
if (SelectedIndexes.Count == 1)
{
RaisePropertyChanged(SelectedIndexProperty, -1, itemIndex, BindingPriority.LocalValue);
}
if (index != -1)
if (SelectedItems.Count != SelectedIndexes.Count)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
MarkContainerSelected(container, false);
var item = Items.Cast<object>().ElementAt(itemIndex);
SelectedItems.Insert(listIndex, item);
}
index = (int)e.NewValue;
var container = MarkIndexSelected(itemIndex, true);
if (index == -1)
if (container != null && Presenter?.Panel != null)
{
SelectedItem = null;
KeyboardNavigation.SetTabOnceActiveElement((InputElement)Presenter.Panel, container);
}
else
}
/// <summary>
/// Called when an index is removed from the <see cref="SelectedIndexes"/> collection.
/// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param>
/// <param name="itemIndex">The item index.</param>
private void SelectedIndexRemoved(int listIndex, int itemIndex)
{
if (SelectedIndexes.Count == 0)
{
SelectedItem = Items.Cast<object>().ElementAt((int)e.NewValue);
var container = ItemContainerGenerator.ContainerFromIndex(index);
MarkContainerSelected(container, true);
RaisePropertyChanged(SelectedIndexProperty, itemIndex, -1, BindingPriority.LocalValue);
}
var inputElement = container as IInputElement;
if (inputElement != null && Presenter != null && Presenter.Panel != null)
{
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
inputElement);
}
if (SelectedIndexes.Count != SelectedItems.Count)
{
SelectedItems.RemoveAt(listIndex);
}
MarkIndexSelected(itemIndex, false);
}
/// <summary>
/// Called when the <see cref="SelectedItem"/> property changes.
/// Called when an item is added to the <see cref="SelectedItems"/> collection.
/// </summary>
/// <param name="e">The event args.</param>
private void SelectedItemChanged(PerspexPropertyChangedEventArgs e)
/// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</param>
private void SelectedItemAdded(int index, object item)
{
SelectedIndex = IndexOf(Items, e.NewValue);
if (SelectedItems.Count == 1)
{
RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue);
}
if (SelectedIndexes.Count != SelectedItems.Count)
{
SelectedIndexes.Insert(index, IndexOf(Items, item));
}
}
/// <summary>
/// Tries to get the container that was the source of an event.
/// Called when an item is removed from the <see cref="SelectedItems"/> collection.
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <returns>The container or null if the event did not originate in a container.</returns>
private IControl GetContainerFromEvent(IInteractive eventSource)
/// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</param>
private void SelectedItemRemoved(int index, object item)
{
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
.OfType<ILogical>()
.FirstOrDefault(x => x.LogicalParent == this);
if (SelectedIndexes.Count != SelectedItems.Count)
{
SelectedIndexes.RemoveAt(index);
}
}
return item as IControl;
/// <summary>
/// Called when the <see cref="SelectedItems"/> collection is reset.
/// </summary>
private void SelectionReset()
{
if (SelectedIndexes.Count > 0)
{
SelectedIndexes.Clear();
}
if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
foreach (var container in ItemContainerGenerator.Containers)
{
MarkContainerSelected(container, false);
}
}
/// <summary>
/// Validates items added to the <see cref="SelectedIndexes"/> collection.
/// </summary>
/// <param name="index">The index to be added.</param>
private void ValidateIndex(int index)
{
if (index < 0 || index >= Items?.Cast<object>().Count())
{
throw new IndexOutOfRangeException();
}
}
/// <summary>
@ -429,32 +598,5 @@ namespace Perspex.Controls.Primitives
SelectedIndex = -1;
}
/// <summary>
/// Tries to set the selection to a container that raised an event.
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <param name="select">Whether the container should be selected or unselected.</param>
private void TrySetSelectionFromContainerEvent(IInteractive eventSource, bool select)
{
var item = GetContainerFromEvent(eventSource);
if (item != null)
{
var index = ItemContainerGenerator.IndexFromContainer(item);
if (index != -1)
{
if (select)
{
SelectedIndex = index;
}
else
{
LostSelection();
}
}
}
}
}
}

23
src/Perspex.Controls/Primitives/TabStrip.cs

@ -5,6 +5,7 @@ using System;
using System.Linq;
using System.Reactive.Linq;
using Perspex.Controls.Generators;
using Perspex.Input;
namespace Perspex.Controls.Primitives
{
@ -47,5 +48,27 @@ namespace Perspex.Controls.Primitives
return result;
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (e.NavigationMethod == NavigationMethod.Directional)
{
UpdateSelectionFromEventSource(e.Source, true);
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressEventArgs e)
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left)
{
UpdateSelectionFromEventSource(e.Source, true);
}
}
}
}

19
tests/Perspex.Controls.UnitTests/ListBoxTests.cs

@ -1,9 +1,7 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Perspex.Controls;
using Perspex.Controls.Presenters;
using Perspex.Controls.Templates;
using Perspex.LogicalTree;
@ -33,23 +31,6 @@ namespace Perspex.Controls.UnitTests
}
}
[Fact]
public void Setting_Item_IsSelected_Sets_ListBox_Selection()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
((ListBoxItem)target.GetLogicalChildren().ElementAt(1)).IsSelected = true;
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void DataContexts_Should_Be_Correctly_Set()
{

188
tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs

@ -0,0 +1,188 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
using Perspex.Controls.Presenters;
using Perspex.Controls.Templates;
using Perspex.Input;
using Perspex.LogicalTree;
using Perspex.Styling;
using Xunit;
namespace Perspex.Controls.UnitTests
{
public class ListBoxTests_Single
{
[Fact]
public void Focusing_Item_With_Tab_Should_Not_Select_It()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Tab,
});
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Focusing_Item_With_Arrow_Key_Should_Select_It()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Directional,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Item_Should_Select_It()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Selected_Item_Should_Not_Deselect_It()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.SelectedIndex = 0;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Item_Should_Select_It_When_SelectionMode_Toggle()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Single | SelectionMode.Toggle,
};
target.ApplyTemplate();
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Selected_Item_Should_Deselect_It_When_SelectionMode_Toggle()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Single | SelectionMode.Toggle,
};
target.ApplyTemplate();
target.SelectedIndex = 0;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Setting_Item_IsSelected_Sets_ListBox_Selection()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
((ListBoxItem)target.GetLogicalChildren().ElementAt(1)).IsSelected = true;
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
private Control CreateListBoxTemplate(ITemplatedControl parent)
{
return new ScrollViewer
{
Template = new ControlTemplate(CreateScrollViewerTemplate),
Content = new ItemsPresenter
{
Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty),
}
};
}
private Control CreateScrollViewerTemplate(ITemplatedControl parent)
{
return new ScrollContentPresenter
{
[~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty),
};
}
private class Item
{
public Item(string value)
{
Value = value;
}
public string Value { get; }
}
}
}

2
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -84,6 +84,8 @@
<Compile Include="GridLengthTests.cs" />
<Compile Include="ContentPresenterTests.cs" />
<Compile Include="BorderTests.cs" />
<Compile Include="ListBoxTests_Single.cs" />
<Compile Include="Primitives\SelectingItemsControlTests_Multiple.cs" />
<Compile Include="TreeViewTests.cs" />
<Compile Include="Mixins\SelectableMixinTests.cs" />
<Compile Include="StackPanelTests.cs" />

95
tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -330,76 +330,6 @@ namespace Perspex.Controls.UnitTests.Primitives
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Focusing_Item_With_Pointer_Should_Select_It()
{
var target = new SelectingItemsControl
{
Template = Template(),
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
var e = new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Pointer,
};
target.Presenter.Panel.Children[1].RaiseEvent(e);
Assert.Equal(1, target.SelectedIndex);
// GotFocus should be raised on parent control.
Assert.False(e.Handled);
}
[Fact]
public void Focusing_Item_With_Directional_Keys_Should_Select_It()
{
var target = new SelectingItemsControl
{
Template = Template(),
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
var e = new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Directional,
};
target.Presenter.Panel.Children[1].RaiseEvent(e);
Assert.Equal(1, target.SelectedIndex);
Assert.False(e.Handled);
}
[Fact]
public void Focusing_Item_With_Tab_Should_Not_Select_It()
{
var target = new SelectingItemsControl
{
Template = Template(),
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
var e = new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Tab,
};
target.Presenter.Panel.Children[1].RaiseEvent(e);
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection()
{
@ -490,6 +420,31 @@ namespace Perspex.Controls.UnitTests.Primitives
Assert.Equal(target.SelectedItem, items[1]);
}
[Fact]
public void Setting_SelectedItem_Should_Set_Panel_Keyboard_Navigation()
{
var items = new[]
{
new Item(),
new Item(),
};
var target = new SelectingItemsControl
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItem = items[1];
var panel = target.Presenter.Panel;
Assert.Equal(
KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel),
panel.Children[1]);
}
private ControlTemplate Template()
{
return new ControlTemplate<SelectingItemsControl>(control =>

246
tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -0,0 +1,246 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Collections;
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Xunit;
namespace Perspex.Controls.UnitTests.Primitives
{
public class SelectingItemsControlTests_Multiple
{
[Fact]
public void Setting_SelectedIndex_Should_Add_To_SelectedIndexes()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndex = 1;
Assert.Equal(new[] { 1 }, target.SelectedIndexes);
}
[Fact]
public void Adding_SelectedIndexes_Should_Set_SelectedIndex()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Adding_First_SelectedIndex_Should_Raise_SelectedIndex_SelectedItem_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
bool indexRaised = false;
bool itemRaised = false;
target.PropertyChanged += (s, e) =>
{
indexRaised |= e.Property.Name == "SelectedIndex" &&
(int)e.OldValue == -1 &&
(int)e.NewValue == 1;
itemRaised |= e.Property.Name == "SelectedItem" &&
(string)e.OldValue == null &&
(string)e.NewValue == "bar";
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
Assert.True(indexRaised);
Assert.True(itemRaised);
}
[Fact]
public void Adding_Subsequent_SelectedIndexes_Should_Not_Raise_SelectedIndex_SelectedItem_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(0);
bool raised = false;
target.PropertyChanged += (s, e) =>
raised |= e.Property.Name == "SelectedIndex" ||
e.Property.Name == "SelectedItem";
target.SelectedIndexes.Add(1);
Assert.False(raised);
}
[Fact]
public void Adding_First_SelectedItem_Should_Raise_SelectedIndex_SelectedItem_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
bool indexRaised = false;
bool itemRaised = false;
target.PropertyChanged += (s, e) =>
{
indexRaised |= e.Property.Name == "SelectedIndex" &&
(int)e.OldValue == -1 &&
(int)e.NewValue == 1;
itemRaised |= e.Property.Name == "SelectedItem" &&
(string)e.OldValue == null &&
(string)e.NewValue == "bar";
};
target.ApplyTemplate();
target.SelectedItems.Add("bar");
Assert.True(indexRaised);
Assert.True(itemRaised);
}
[Fact]
public void Removing_Last_SelectedIndex_Should_Raise_SelectedIndex_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(0);
bool raised = false;
target.PropertyChanged += (s, e) =>
raised = e.Property.Name == "SelectedIndex" &&
(int)e.OldValue == 0 &&
(int)e.NewValue == -1;
target.SelectedIndexes.RemoveAt(0);
Assert.True(raised);
}
[Fact]
public void Adding_To_SelectedIndexes_Should_Add_To_SelectedItems()
{
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
},
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
Assert.Equal(new[] { "bar" }, target.SelectedItems);
}
[Fact]
public void Adding_To_SelectedItems_Should_Add_To_SelectedIndexes()
{
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
},
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItems.Add("bar");
Assert.Equal(new[] { 1 }, target.SelectedIndexes);
}
[Fact]
public void Adding_SelectedIndexes_Should_Set_Item_IsSelected()
{
var target = new TestSelector
{
Items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
},
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
Assert.True(((ListBoxItem)target.Presenter.Panel.Children[1]).IsSelected);
}
[Fact]
public void Removing_SelectedIndexes_Should_Clear_Item_IsSelected()
{
var target = new TestSelector
{
Items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
},
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
target.SelectedIndexes.Remove(1);
Assert.False(((ListBoxItem)target.Presenter.Panel.Children[1]).IsSelected);
}
private class TestSelector : SelectingItemsControl
{
public new IPerspexList<int> SelectedIndexes
{
get { return base.SelectedIndexes; }
}
public new IPerspexList<object> SelectedItems
{
get { return base.SelectedItems; }
}
}
private ControlTemplate Template()
{
return new ControlTemplate<SelectingItemsControl>(control =>
new ItemsPresenter
{
Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
[~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
});
}
}
}
Loading…
Cancel
Save