Browse Source

Merge branch 'multiple-selection'. Missed some commits before.

Conflicts:
	samples/BindingTest/MainWindow.paml
pull/278/head
Steven Kirk 11 years ago
parent
commit
f279174841
  1. 76
      samples/BindingTest/MainWindow.paml
  2. 15
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  3. 15
      src/Perspex.Controls/ListBox.cs
  4. 455
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  5. 13
      src/Perspex.Controls/TabControl.cs
  6. 25
      tests/Perspex.Controls.UnitTests/ListBoxTests.cs
  7. 25
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  8. 214
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

76
samples/BindingTest/MainWindow.paml

@ -1,29 +1,53 @@
<Window xmlns="https://github.com/perspex">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding StringValue}"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneTime}"/>
<TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWayToSource}"/>
<Window xmlns="https://github.com/perspex"
xmlns:vm="clr-namespace:BindingTest.ViewModels;assembly=BindingTest">
<TabControl>
<TabItem Header="Basic">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
<TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWayToSource}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Collection Bindings"/>
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Path=Items[1].StringValue}"/>
<Button Command="{Binding ShuffleItems}">Shuffle</Button>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Negated Bindings"/>
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding Path=BooleanString}"/>
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
<CheckBox IsChecked="{Binding !!BooleanString}">!!BooleanString</CheckBox>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Numeric Bindings"/>
<TextBox Watermark="Double" UseFloatingWatermark="True" Text="{Binding Path=DoubleValue, Mode=TwoWay}"/>
<TextBlock Text="{Binding Path=DoubleValue}"/>
</StackPanel>
</StackPanel>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Collection Bindings"/>
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Items[1].StringValue}"/>
<Button Command="{Binding ShuffleItems}">Shuffle</Button>
</TabItem>
<TabItem Header="ListBox">
<StackPanel Orientation="Horizontal">
<StackPanel.DataTemplates>
<DataTemplate DataType="vm:TestItem">
<TextBlock Text="{Binding StringValue}"/>
</DataTemplate>
</StackPanel.DataTemplates>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
</StackPanel>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Negated Bindings"/>
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding BooleanString}"/>
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
<CheckBox IsChecked="{Binding !!BooleanString}">!!BooleanString</CheckBox>
</StackPanel>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Numeric Bindings"/>
<TextBox Watermark="Double" UseFloatingWatermark="True" Text="{Binding DoubleValue, Mode=TwoWay}"/>
<TextBlock Text="{Binding DoubleValue}"/>
</StackPanel>
</StackPanel>
</TabItem>
</TabControl>
</Window>

15
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using ReactiveUI;
namespace BindingTest.ViewModels
@ -12,12 +13,13 @@ namespace BindingTest.ViewModels
public MainWindowViewModel()
{
Items = new ObservableCollection<TestItem>
{
new TestItem { StringValue = "Foo" },
new TestItem { StringValue = "Bar" },
new TestItem { StringValue = "Baz" },
};
Items = new ObservableCollection<TestItem>(
Enumerable.Range(0, 20).Select(x => new TestItem
{
StringValue = "Item " + x
}));
SelectedItems = new ObservableCollection<TestItem>();
ShuffleItems = ReactiveCommand.Create();
ShuffleItems.Subscribe(_ =>
@ -28,6 +30,7 @@ namespace BindingTest.ViewModels
}
public ObservableCollection<TestItem> Items { get; }
public ObservableCollection<TestItem> SelectedItems { get; }
public ReactiveCommand<object> ShuffleItems { get; }
public string BooleanString

15
src/Perspex.Controls/ListBox.cs

@ -1,6 +1,9 @@
// 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.Collections;
using System.Collections.Generic;
using Perspex.Collections;
using Perspex.Controls.Generators;
using Perspex.Controls.Primitives;
using Perspex.Input;
@ -12,12 +15,24 @@ namespace Perspex.Controls
/// </summary>
public class ListBox : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
/// </summary>
public static readonly new PerspexProperty<IList> SelectedItemsProperty =
SelectingItemsControl.SelectedItemsProperty;
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
public static readonly new PerspexProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
/// <inheritdoc/>
public new IList SelectedItems
{
get { return base.SelectedItems; }
}
/// <inheritdoc/>
public new SelectionMode SelectionMode
{

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

@ -23,9 +23,9 @@ namespace Perspex.Controls.Primitives
/// <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.
/// current multiple selection <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
@ -54,21 +54,14 @@ namespace Perspex.Controls.Primitives
o => o.SelectedItem,
(o, v) => o.SelectedItem = v);
/// <summary>
/// Defines the <see cref="SelectedIndexes"/> property.
/// </summary>
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<IPerspexList<object>> SelectedItemsProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IPerspexList<object>>(
protected static readonly PerspexProperty<IList> SelectedItemsProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IList>(
nameof(SelectedItems),
o => o.SelectedItems);
o => o.SelectedItems,
(o, v) => o.SelectedItems = v);
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
@ -85,8 +78,9 @@ namespace Perspex.Controls.Primitives
public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>("IsSelectedChanged", RoutingStrategies.Bubble);
private PerspexList<int> _selectedIndexes = new PerspexList<int>();
private PerspexList<object> _selectedItems = new PerspexList<object>();
private int _selectedIndex = -1;
private object _selectedItem;
private IList _selectedItems;
private bool _ignoreContainerSelectionChanged;
/// <summary>
@ -103,9 +97,6 @@ namespace Perspex.Controls.Primitives
public SelectingItemsControl()
{
ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized);
_selectedIndexes.Validate = ValidateIndex;
_selectedIndexes.ForEachItem(SelectedIndexesAdded, SelectedIndexesRemoved, SelectionReset);
_selectedItems.ForEachItem(SelectedItemsAdded, SelectedItemsRemoved, SelectionReset);
}
/// <summary>
@ -115,7 +106,7 @@ namespace Perspex.Controls.Primitives
{
get
{
return _selectedIndexes.Count > 0 ? _selectedIndexes[0]: -1;
return _selectedIndex;
}
set
@ -125,14 +116,9 @@ namespace Perspex.Controls.Primitives
if (old != effective)
{
_selectedIndexes.Clear();
if (effective != -1)
{
_selectedIndexes.Add(effective);
}
_selectedIndex = effective;
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
SelectedItem = ElementAt(Items, effective);
}
}
}
@ -144,42 +130,62 @@ namespace Perspex.Controls.Primitives
{
get
{
return _selectedItems.FirstOrDefault();
return _selectedItem;
}
set
{
var old = SelectedItem;
var effective = Items?.Cast<object>().Contains(value) == true ? value : null;
var index = IndexOf(Items, value);
var effective = index != -1 ? value : null;
if (effective != old)
{
_selectedItems.Clear();
_selectedItem = effective;
RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
SelectedIndex = index;
if (effective != null)
{
_selectedItems.Add(effective);
if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
{
SelectedItems.Clear();
SelectedItems.Add(effective);
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
}
}
}
/// <summary>
/// Gets the selected indexes.
/// </summary>
protected IPerspexList<int> SelectedIndexes
{
get { return _selectedIndexes; }
}
/// <summary>
/// Gets the selected items.
/// </summary>
protected IPerspexList<object> SelectedItems
protected IList SelectedItems
{
get { return _selectedItems; }
get
{
if (_selectedItems == null)
{
_selectedItems = new PerspexList<object>();
SubscribeToSelectedItems();
}
return _selectedItems;
}
set
{
if (value != null)
{
UnsubscribeFromSelectedItems();
_selectedItems = value;
SubscribeToSelectedItems();
}
}
}
/// <summary>
@ -285,7 +291,7 @@ namespace Perspex.Controls.Primitives
var mode = SelectionMode;
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && SelectedIndexes.Count > 0 ? rangeModifier : false;
var range = multi && SelectedIndex != -1 ? rangeModifier : false;
if (!toggle && !range)
{
@ -293,21 +299,24 @@ namespace Perspex.Controls.Primitives
}
else if (multi && range)
{
SynchronizeIndexes(SelectedIndexes, SelectedIndexes[0], index);
SynchronizeItems(
SelectedItems,
GetRange(Items, SelectedIndex, index));
}
else
{
var i = SelectedIndexes.IndexOf(index);
var item = ElementAt(Items, index);
var i = SelectedItems.IndexOf(item);
if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
{
SelectedIndexes.RemoveAt(i);
SelectedItems.Remove(item);
}
else
{
if (multi)
{
SelectedIndexes.Add(index);
SelectedItems.Add(item);
}
else
{
@ -315,6 +324,14 @@ namespace Perspex.Controls.Primitives
}
}
}
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
}
}
else
{
@ -373,6 +390,26 @@ namespace Perspex.Controls.Primitives
return false;
}
/// <summary>
/// Gets the item at the specified index in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="index">The index.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
private static object ElementAt(IEnumerable items, int index)
{
var typedItems = items?.Cast<object>();
if (index != -1 && typedItems != null && index < typedItems.Count())
{
return typedItems.ElementAt(index) ?? null;
}
else
{
return null;
}
}
/// <summary>
/// Gets the index of an item in a collection.
/// </summary>
@ -409,85 +446,54 @@ namespace Perspex.Controls.Primitives
}
/// <summary>
/// Generates a range of integers between the first and last inclusive.
/// Gets a range of items from an IEnumerable.
/// </summary>
/// <param name="first">The first integer.</param>
/// <param name="last">The last integer.</param>
/// <returns>The range.</returns>
private static IEnumerable<int> Range(int first, int last)
/// <param name="items">The items.</param>
/// <param name="first">The index of the first item.</param>
/// <param name="last">The index of the last item.</param>
/// <returns>The items.</returns>
private static IEnumerable<object> GetRange(IEnumerable items, int first, int last)
{
var list = (items as IList) ?? items.Cast<object>().ToList();
int step = first > last ? -1 : 1;
for (int i = first; i != last; i += step)
{
yield return i;
yield return list[i];
}
yield return last;
yield return list[last];
}
/// <summary>
/// Makes a list of integers equal the range first...last.
/// Makes a list of objects equal another.
/// </summary>
/// <param name="indexes">The list of indexes.</param>
/// <param name="first">The first in the range.</param>
/// <param name="last">The last in the range.</param>
private static void SynchronizeIndexes(IPerspexList<int> indexes, int first, int last)
/// <param name="items">The items collection.</param>
/// <param name="desired">The desired items.</param>
private static void SynchronizeItems(IList items, IEnumerable<object> desired)
{
var i = 0;
var next = first;
int step = first > last ? -1 : 1;
int index = 0;
while (i < indexes.Count && indexes[i] == next && next != last)
foreach (var i in desired)
{
++i;
next += step;
}
if (next != last || i != indexes.Count - 1)
{
if (i < indexes.Count - 1)
if (index < items.Count)
{
indexes.RemoveRange(i, indexes.Count - i);
}
indexes.AddRange(Range(next, last));
}
}
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param>
private void MarkContainerSelected(IControl container, bool selected)
{
try
{
var selectable = container as ISelectable;
var styleable = container as IStyleable;
_ignoreContainerSelectionChanged = true;
if (selectable != null)
{
selectable.IsSelected = selected;
}
else if (styleable != null)
{
if (selected)
{
styleable.Classes.Add(":selected");
}
else
if (items[index] != i)
{
styleable.Classes.Remove(":selected");
items[index] = i;
}
}
else
{
items.Add(i);
}
++index;
}
finally
while (index < items.Count)
{
_ignoreContainerSelectionChanged = false;
items.RemoveAt(items.Count - 1);
}
}
@ -530,165 +536,214 @@ namespace Perspex.Controls.Primitives
}
/// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// Called when the currently selected item is lost and the selection must be changed
/// depending on the <see cref="SelectionMode"/> property.
/// </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)
private void LostSelection()
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
var items = Items?.Cast<object>();
if (container != null)
if (items != null && AlwaysSelected)
{
MarkContainerSelected(container, selected);
var index = Math.Min(SelectedIndex, items.Count() - 1);
if (index > -1)
{
SelectedItem = items.ElementAt(index);
return;
}
}
return container;
SelectedIndex = -1;
}
/// <summary>
/// Called when an index is added to the <see cref="SelectedIndexes"/> collection.
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param>
/// <param name="itemIndexes">The item indexes.</param>
private void SelectedIndexesAdded(int listIndex, IEnumerable<int> itemIndexes)
/// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param>
private void MarkContainerSelected(IControl container, bool selected)
{
var indexes = (itemIndexes as IList<int>) ?? itemIndexes.ToList();
IControl container = null;
if (SelectedItems.Count != SelectedIndexes.Count)
try
{
var items = indexes.Select(x => Items.Cast<object>().ElementAt(x));
SelectedItems.AddRange(items);
}
var selectable = container as ISelectable;
var styleable = container as IStyleable;
foreach (var itemIndex in indexes)
{
container = MarkIndexSelected(itemIndex, true);
}
_ignoreContainerSelectionChanged = true;
if (SelectedIndexes.Count == 1)
{
RaisePropertyChanged(SelectedIndexProperty, -1, SelectedIndexes[0], BindingPriority.LocalValue);
if (selectable != null)
{
selectable.IsSelected = selected;
}
else if (styleable != null)
{
if (selected)
{
styleable.Classes.Add(":selected");
}
else
{
styleable.Classes.Remove(":selected");
}
}
}
if (container != null && Presenter?.Panel != null)
finally
{
KeyboardNavigation.SetTabOnceActiveElement((InputElement)Presenter.Panel, container);
_ignoreContainerSelectionChanged = false;
}
}
/// <summary>
/// Called when an index is removed from the <see cref="SelectedIndexes"/> collection.
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param>
/// <param name="itemIndexes">The item indexes.</param>
private void SelectedIndexesRemoved(int listIndex, IEnumerable<int> itemIndexes)
/// <param name="index">The index of the item.</param>
/// <param name="selected">Whether the item should be selected or deselected.</param>
private void MarkItemSelected(int index, bool selected)
{
var sync = SelectedIndexes.Count != SelectedItems.Count;
SelectedItems.RemoveRange(listIndex, itemIndexes.Count());
foreach (var itemIndex in itemIndexes)
{
MarkIndexSelected(itemIndex, false);
}
var container = ItemContainerGenerator.ContainerFromIndex(index);
if (SelectedIndexes.Count == 0)
if (container != null)
{
RaisePropertyChanged(
SelectedIndexProperty,
itemIndexes.First(),
-1,
BindingPriority.LocalValue);
MarkContainerSelected(container, selected);
}
}
/// <summary>
/// Called when an item is added to the <see cref="SelectedItems"/> collection.
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</param>
private void SelectedItemsAdded(int index, object item)
/// <param name="selected">Whether the item should be selected or deselected.</param>
private void MarkItemSelected(object item, bool selected)
{
if (SelectedIndexes.Count != SelectedItems.Count)
{
SelectedIndexes.Insert(index, IndexOf(Items, item));
}
var index = IndexOf(Items, item);
if (SelectedItems.Count == 1)
if (index != -1)
{
RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue);
MarkItemSelected(index, selected);
}
}
/// <summary>
/// Called when an item is removed from the <see cref="SelectedItems"/> collection.
/// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
/// </summary>
/// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</param>
private void SelectedItemsRemoved(int index, object item)
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (SelectedIndexes.Count != SelectedItems.Count)
switch (e.Action)
{
SelectedIndexes.RemoveAt(index);
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().ToList());
break;
case NotifyCollectionChangedAction.Remove:
if (SelectedItems.Count == 0)
{
SelectedIndex = -1;
}
else
{
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
}
break;
case NotifyCollectionChangedAction.Reset:
foreach (var item in ItemContainerGenerator.Containers)
{
MarkContainerSelected(item, false);
}
SelectedIndex = -1;
SelectedItemsAdded(SelectedItems);
break;
case NotifyCollectionChangedAction.Replace:
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
foreach (var item in e.NewItems)
{
MarkItemSelected(item, true);
}
if (SelectedItem != SelectedItems[0])
{
var oldItem = SelectedItem;
var oldIndex = SelectedIndex;
var item = SelectedItems[0];
var index = IndexOf(Items, item);
_selectedIndex = index;
_selectedItem = item;
RaisePropertyChanged(SelectedIndexProperty, oldIndex, index, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, oldItem, item, BindingPriority.LocalValue);
}
break;
}
}
/// <summary>
/// Called when the <see cref="SelectedItems"/> collection is reset.
/// Called when items are added to the <see cref="SelectedItems"/> collection.
/// </summary>
private void SelectionReset()
/// <param name="items">The added items.</param>
private void SelectedItemsAdded(IList items)
{
if (SelectedIndexes.Count > 0)
if (items.Count > 0)
{
SelectedIndexes.Clear();
}
foreach (var item in items)
{
MarkItemSelected(item, true);
}
if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
if (SelectedItem == null)
{
var index = IndexOf(Items, items[0]);
foreach (var container in ItemContainerGenerator.Containers)
{
MarkContainerSelected(container, false);
if (index != -1)
{
_selectedItem = items[0];
_selectedIndex = index;
RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue);
}
}
}
}
/// <summary>
/// Validates items added to the <see cref="SelectedIndexes"/> collection.
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
/// <param name="index">The index to be added.</param>
private void ValidateIndex(int index)
private void SubscribeToSelectedItems()
{
if (index < 0 || index >= Items?.Cast<object>().Count())
var incc = _selectedItems as INotifyCollectionChanged;
if (incc != null)
{
throw new IndexOutOfRangeException();
incc.CollectionChanged += SelectedItemsCollectionChanged;
}
SelectedItemsCollectionChanged(
_selectedItems,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Called when the currently selected item is lost and the selection must be changed
/// depending on the <see cref="SelectionMode"/> property.
/// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
private void LostSelection()
private void UnsubscribeFromSelectedItems()
{
var items = Items?.Cast<object>();
var incc = _selectedItems as INotifyCollectionChanged;
if (items != null && AlwaysSelected)
if (incc != null)
{
var index = Math.Min(SelectedIndex, items.Count() - 1);
if (index > -1)
{
SelectedItem = items.ElementAt(index);
return;
}
incc.CollectionChanged -= SelectedItemsCollectionChanged;
}
SelectedIndex = -1;
}
}
}

13
src/Perspex.Controls/TabControl.cs

@ -35,7 +35,7 @@ namespace Perspex.Controls
{
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue<TabControl>(false);
SelectedIndexProperty.Changed.AddClassHandler<TabControl>(x => x.SelectedIndexChanged);
SelectedItemProperty.Changed.AddClassHandler<TabControl>(x => x.SelectedItemChanged);
}
/// <summary>
@ -100,14 +100,11 @@ namespace Perspex.Controls
/// Called when the <see cref="SelectingItemsControl.SelectedIndex"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e)
private void SelectedItemChanged(PerspexPropertyChangedEventArgs e)
{
if ((int)e.NewValue != -1)
{
var item = SelectedItem as IContentControl;
var content = item?.Content ?? item;
SelectedTab = item as TabItem;
}
var item = e.NewValue as IContentControl;
var content = item?.Content ?? item;
SelectedTab = item as TabItem;
}
}
}

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

@ -4,6 +4,7 @@
using System.Linq;
using Perspex.Controls.Presenters;
using Perspex.Controls.Templates;
using Perspex.Input;
using Perspex.LogicalTree;
using Perspex.Styling;
using Xunit;
@ -65,6 +66,30 @@ namespace Perspex.Controls.UnitTests
dataContexts);
}
[Fact]
public void Setting_SelectedItem_Should_Set_Panel_Keyboard_Navigation()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
var panel = target.Presenter.Panel;
Assert.Equal(
KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel),
panel.Children[1]);
}
private Control CreateListBoxTemplate(ITemplatedControl parent)
{
return new ScrollViewer

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

@ -420,31 +420,6 @@ 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 =>

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

@ -1,6 +1,9 @@
// 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.Collections;
using System.Collections.Generic;
using System.Linq;
using Perspex.Collections;
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
@ -12,7 +15,7 @@ namespace Perspex.Controls.UnitTests.Primitives
public class SelectingItemsControlTests_Multiple
{
[Fact]
public void Setting_SelectedIndex_Should_Add_To_SelectedIndexes()
public void Setting_SelectedIndex_Should_Add_To_SelectedItems()
{
var target = new TestSelector
{
@ -23,11 +26,11 @@ namespace Perspex.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.SelectedIndex = 1;
Assert.Equal(new[] { 1 }, target.SelectedIndexes);
Assert.Equal(new[] { "bar" }, target.SelectedItems.Cast<object>().ToList());
}
[Fact]
public void Adding_SelectedIndexes_Should_Set_SelectedIndex()
public void Adding_SelectedItems_Should_Set_SelectedIndex()
{
var target = new TestSelector
{
@ -36,13 +39,13 @@ namespace Perspex.Controls.UnitTests.Primitives
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
target.SelectedItems.Add("bar");
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Adding_First_SelectedIndex_Should_Raise_SelectedIndex_SelectedItem_Changed()
public void Assigning_SelectedItems_Should_Set_SelectedIndex()
{
var target = new TestSelector
{
@ -50,27 +53,14 @@ namespace Perspex.Controls.UnitTests.Primitives
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);
target.SelectedItems = new[] { "bar" };
Assert.True(indexRaised);
Assert.True(itemRaised);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Adding_Subsequent_SelectedIndexes_Should_Not_Raise_SelectedIndex_SelectedItem_Changed()
public void Reassigning_SelectedItems_Should_Clear_Selection()
{
var target = new TestSelector
{
@ -79,16 +69,11 @@ namespace Perspex.Controls.UnitTests.Primitives
};
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);
target.SelectedItems.Add("bar");
target.SelectedItems = new PerspexList<object>();
Assert.False(raised);
Assert.Equal(-1, target.SelectedIndex);
Assert.Equal(null, target.SelectedItem);
}
[Fact]
@ -120,7 +105,7 @@ namespace Perspex.Controls.UnitTests.Primitives
}
[Fact]
public void Removing_Last_SelectedIndex_Should_Raise_SelectedIndex_Changed()
public void Adding_Subsequent_SelectedItems_Should_Not_Raise_SelectedIndex_SelectedItem_Changed()
{
var target = new TestSelector
{
@ -129,94 +114,168 @@ namespace Perspex.Controls.UnitTests.Primitives
};
target.ApplyTemplate();
target.SelectedIndexes.Add(0);
target.SelectedItems.Add("foo");
bool raised = false;
target.PropertyChanged += (s, e) =>
raised = e.Property.Name == "SelectedIndex" &&
(int)e.OldValue == 0 &&
(int)e.NewValue == -1;
raised |= e.Property.Name == "SelectedIndex" ||
e.Property.Name == "SelectedItem";
target.SelectedIndexes.RemoveAt(0);
target.SelectedItems.Add("bar");
Assert.False(raised);
}
[Fact]
public void Removing_Last_SelectedItem_Should_Raise_SelectedIndex_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItems.Add("foo");
bool raised = false;
target.PropertyChanged += (s, e) =>
raised |= e.Property.Name == "SelectedIndex" &&
(int)e.OldValue == 0 &&
(int)e.NewValue == -1;
target.SelectedItems.RemoveAt(0);
Assert.True(raised);
}
[Fact]
public void Adding_To_SelectedIndexes_Should_Add_To_SelectedItems()
public void Adding_SelectedItems_Should_Set_Item_IsSelected()
{
var items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
new ListBoxItem(),
};
var target = new TestSelector
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItems.Add(items[0]);
target.SelectedItems.Add(items[1]);
var foo = target.Presenter.Panel.Children[0];
Assert.True(items[0].IsSelected);
Assert.True(items[1].IsSelected);
Assert.False(items[2].IsSelected);
}
[Fact]
public void Assigning_SelectedItems_Should_Set_Item_IsSelected()
{
var items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
new ListBoxItem(),
};
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
},
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
target.SelectedItems = new PerspexList<object> { items[0], items[1] };
Assert.Equal(new[] { "bar" }, target.SelectedItems);
Assert.True(items[0].IsSelected);
Assert.True(items[1].IsSelected);
Assert.False(items[2].IsSelected);
}
[Fact]
public void Adding_To_SelectedItems_Should_Add_To_SelectedIndexes()
public void Removing_SelectedItems_Should_Clear_Item_IsSelected()
{
var items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
new ListBoxItem(),
};
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
},
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItems.Add("bar");
target.SelectedItems.Add(items[0]);
target.SelectedItems.Add(items[1]);
target.SelectedItems.Remove(items[1]);
Assert.Equal(new[] { 1 }, target.SelectedIndexes);
Assert.True(items[0].IsSelected);
Assert.False(items[1].IsSelected);
}
[Fact]
public void Adding_SelectedIndexes_Should_Set_Item_IsSelected()
public void Reassigning_SelectedItems_Should_Clear_Item_IsSelected()
{
var items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
new ListBoxItem(),
};
var target = new TestSelector
{
Items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
},
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
target.SelectedItems.Add(items[0]);
target.SelectedItems.Add(items[1]);
Assert.True(((ListBoxItem)target.Presenter.Panel.Children[1]).IsSelected);
target.SelectedItems = new PerspexList<object> { items[0], items[1] };
Assert.False(items[0].IsSelected);
Assert.False(items[1].IsSelected);
}
[Fact]
public void Removing_SelectedIndexes_Should_Clear_Item_IsSelected()
public void Replacing_First_SelectedItem_Should_Update_SelectedItem_SelectedIndex()
{
var items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
new ListBoxItem(),
};
var target = new TestSelector
{
Items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
},
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
target.SelectedIndexes.Remove(1);
target.SelectedIndex = 1;
target.SelectedItems[0] = items[2];
Assert.False(((ListBoxItem)target.Presenter.Panel.Children[1]).IsSelected);
Assert.Equal(2, target.SelectedIndex);
Assert.Equal(items[2], target.SelectedItem);
Assert.False(items[0].IsSelected);
Assert.False(items[1].IsSelected);
Assert.True(items[2].IsSelected);
}
[Fact]
@ -241,8 +300,7 @@ namespace Perspex.Controls.UnitTests.Primitives
target.SelectedIndex = 1;
target.SelectRange(3);
Assert.Equal(new[] { 1, 2, 3 }, target.SelectedIndexes);
Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems);
Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems.Cast<object>().ToList());
}
[Fact]
@ -267,8 +325,7 @@ namespace Perspex.Controls.UnitTests.Primitives
target.SelectedIndex = 3;
target.SelectRange(1);
Assert.Equal(new[] { 3, 2, 1 }, target.SelectedIndexes);
Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems);
Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems.Cast<object>().ToList());
}
[Fact]
@ -294,20 +351,15 @@ namespace Perspex.Controls.UnitTests.Primitives
target.SelectRange(5);
target.SelectRange(4);
Assert.Equal(new[] { 2, 3, 4 }, target.SelectedIndexes);
Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems);
Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast<object>().ToList());
}
private class TestSelector : SelectingItemsControl
{
public new IPerspexList<int> SelectedIndexes
{
get { return base.SelectedIndexes; }
}
public new IPerspexList<object> SelectedItems
public new IList SelectedItems
{
get { return base.SelectedItems; }
set { base.SelectedItems = value; }
}
public new SelectionMode SelectionMode

Loading…
Cancel
Save