Browse Source

Make SelectedItems bindable.

And to avoid confusion removed SelectedIndexes.
pull/278/head
Steven Kirk 11 years ago
parent
commit
c0dff57a78
  1. 64
      samples/BindingTest/MainWindow.paml
  2. 15
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  3. 14
      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. 213
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

64
samples/BindingTest/MainWindow.paml

@ -1,22 +1,44 @@
<Window xmlns="https://github.com/perspex"> <Window xmlns="https://github.com/perspex"
<StackPanel Orientation="Horizontal"> xmlns:vm="clr-namespace:BindingTest.ViewModels;assembly=BindingTest">
<StackPanel Margin="18" Gap="4" Width="200"> <TabControl>
<TextBlock FontSize="16" Text="Simple Bindings"/> <TabItem Header="Basic">
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding StringValue}"/> <StackPanel Orientation="Horizontal">
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWay}"/> <StackPanel Margin="18" Gap="4" Width="200">
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneTime}"/> <TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWayToSource}"/> <TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding StringValue}"/>
</StackPanel> <TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<StackPanel Margin="18" Gap="4" Width="200"> <TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
<TextBlock FontSize="16" Text="Collection Bindings"/> <TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWayToSource}"/>
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Items[1].StringValue}"/> </StackPanel>
<Button Command="{Binding ShuffleItems}">Shuffle</Button> <StackPanel Margin="18" Gap="4" Width="200">
</StackPanel> <TextBlock FontSize="16" Text="Collection Bindings"/>
<StackPanel Margin="18" Gap="4" Width="200"> <TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Items[1].StringValue}"/>
<TextBlock FontSize="16" Text="Negated Bindings"/> <Button Command="{Binding ShuffleItems}">Shuffle</Button>
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding BooleanString}"/> </StackPanel>
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox> <StackPanel Margin="18" Gap="4" Width="200">
<CheckBox IsChecked="{Binding !!BooleanString}">!!BooleanString</CheckBox> <TextBlock FontSize="16" Text="Negated Bindings"/>
</StackPanel> <TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding BooleanString}"/>
</StackPanel> <CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
<CheckBox IsChecked="{Binding !!BooleanString}">!!BooleanString</CheckBox>
</StackPanel>
</StackPanel>
</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>
</TabItem>
</TabControl>
</Window> </Window>

15
samples/BindingTest/ViewModels/MainWindowViewModel.cs

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

14
src/Perspex.Controls/ListBox.cs

@ -1,6 +1,8 @@
// Copyright (c) The Perspex Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using Perspex.Collections;
using Perspex.Controls.Generators; using Perspex.Controls.Generators;
using Perspex.Controls.Primitives; using Perspex.Controls.Primitives;
using Perspex.Input; using Perspex.Input;
@ -12,12 +14,24 @@ namespace Perspex.Controls
/// </summary> /// </summary>
public class ListBox : SelectingItemsControl public class ListBox : SelectingItemsControl
{ {
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
/// </summary>
public static readonly new PerspexProperty<IList<object>> SelectedItemsProperty =
SelectingItemsControl.SelectedItemsProperty;
/// <summary> /// <summary>
/// Defines the <see cref="SelectionMode"/> property. /// Defines the <see cref="SelectionMode"/> property.
/// </summary> /// </summary>
public static readonly new PerspexProperty<SelectionMode> SelectionModeProperty = public static readonly new PerspexProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty; SelectingItemsControl.SelectionModeProperty;
/// <inheritdoc/>
public new IList<object> SelectedItems
{
get { return base.SelectedItems; }
}
/// <inheritdoc/> /// <inheritdoc/>
public new SelectionMode SelectionMode 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 /// <see cref="SelectingItemsControl"/> provides a base class for <see cref="ItemsControl"/>s
/// that maintain a selection (single or multiple). By default only its /// that maintain a selection (single or multiple). By default only its
/// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the /// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the
/// multiple selection properties <see cref="SelectedIndexes"/> and <see cref="SelectedItems"/> /// current multiple selection <see cref="SelectedItems"/> together with the
/// together with the <see cref="SelectionMode"/> properties are protected, however a derived /// <see cref="SelectionMode"/> properties are protected, however a derived class can expose
/// class can expose these if it wishes to support multiple selection. /// these if it wishes to support multiple selection.
/// </para> /// </para>
/// <para> /// <para>
/// <see cref="SelectingItemsControl"/> maintains a selection respecting the current /// <see cref="SelectingItemsControl"/> maintains a selection respecting the current
@ -54,21 +54,14 @@ namespace Perspex.Controls.Primitives
o => o.SelectedItem, o => o.SelectedItem,
(o, v) => o.SelectedItem = v); (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> /// <summary>
/// Defines the <see cref="SelectedItems"/> property. /// Defines the <see cref="SelectedItems"/> property.
/// </summary> /// </summary>
protected static readonly PerspexProperty<IPerspexList<object>> SelectedItemsProperty = protected static readonly PerspexProperty<IList<object>> SelectedItemsProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IPerspexList<object>>( PerspexProperty.RegisterDirect<SelectingItemsControl, IList<object>>(
nameof(SelectedItems), nameof(SelectedItems),
o => o.SelectedItems); o => o.SelectedItems,
(o, v) => o.SelectedItems = v);
/// <summary> /// <summary>
/// Defines the <see cref="SelectionMode"/> property. /// Defines the <see cref="SelectionMode"/> property.
@ -85,8 +78,9 @@ namespace Perspex.Controls.Primitives
public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent = public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>("IsSelectedChanged", RoutingStrategies.Bubble); RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>("IsSelectedChanged", RoutingStrategies.Bubble);
private PerspexList<int> _selectedIndexes = new PerspexList<int>(); private int _selectedIndex = -1;
private PerspexList<object> _selectedItems = new PerspexList<object>(); private object _selectedItem;
private IList<object> _selectedItems;
private bool _ignoreContainerSelectionChanged; private bool _ignoreContainerSelectionChanged;
/// <summary> /// <summary>
@ -103,9 +97,6 @@ namespace Perspex.Controls.Primitives
public SelectingItemsControl() public SelectingItemsControl()
{ {
ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized);
_selectedIndexes.Validate = ValidateIndex;
_selectedIndexes.ForEachItem(SelectedIndexesAdded, SelectedIndexesRemoved, SelectionReset);
_selectedItems.ForEachItem(SelectedItemsAdded, SelectedItemsRemoved, SelectionReset);
} }
/// <summary> /// <summary>
@ -115,7 +106,7 @@ namespace Perspex.Controls.Primitives
{ {
get get
{ {
return _selectedIndexes.Count > 0 ? _selectedIndexes[0]: -1; return _selectedIndex;
} }
set set
@ -125,14 +116,9 @@ namespace Perspex.Controls.Primitives
if (old != effective) if (old != effective)
{ {
_selectedIndexes.Clear(); _selectedIndex = effective;
if (effective != -1)
{
_selectedIndexes.Add(effective);
}
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
SelectedItem = ElementAt(Items, effective);
} }
} }
} }
@ -144,42 +130,62 @@ namespace Perspex.Controls.Primitives
{ {
get get
{ {
return _selectedItems.FirstOrDefault(); return _selectedItem;
} }
set set
{ {
var old = SelectedItem; 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) if (effective != old)
{ {
_selectedItems.Clear(); _selectedItem = effective;
RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
SelectedIndex = index;
if (effective != null) if (effective != null)
{ {
_selectedItems.Add(effective); if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
{
SelectedItems.Clear();
SelectedItems.Add(effective);
}
}
else
{
SelectedItems.Clear();
} }
RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
} }
} }
} }
/// <summary>
/// Gets the selected indexes.
/// </summary>
protected IPerspexList<int> SelectedIndexes
{
get { return _selectedIndexes; }
}
/// <summary> /// <summary>
/// Gets the selected items. /// Gets the selected items.
/// </summary> /// </summary>
protected IPerspexList<object> SelectedItems protected IList<object> SelectedItems
{ {
get { return _selectedItems; } get
{
if (_selectedItems == null)
{
_selectedItems = new PerspexList<object>();
SubscribeToSelectedItems();
}
return _selectedItems;
}
set
{
if (value != null)
{
UnsubscribeFromSelectedItems();
_selectedItems = value;
SubscribeToSelectedItems();
}
}
} }
/// <summary> /// <summary>
@ -285,7 +291,7 @@ namespace Perspex.Controls.Primitives
var mode = SelectionMode; var mode = SelectionMode;
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 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) if (!toggle && !range)
{ {
@ -293,21 +299,24 @@ namespace Perspex.Controls.Primitives
} }
else if (multi && range) else if (multi && range)
{ {
SynchronizeIndexes(SelectedIndexes, SelectedIndexes[0], index); SynchronizeItems(
SelectedItems,
GetRange(Items, SelectedIndex, index));
} }
else else
{ {
var i = SelectedIndexes.IndexOf(index); var item = ElementAt(Items, index);
var i = SelectedItems.IndexOf(item);
if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1)) if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
{ {
SelectedIndexes.RemoveAt(i); SelectedItems.Remove(item);
} }
else else
{ {
if (multi) if (multi)
{ {
SelectedIndexes.Add(index); SelectedItems.Add(item);
} }
else 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 else
{ {
@ -373,6 +390,26 @@ namespace Perspex.Controls.Primitives
return false; 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> /// <summary>
/// Gets the index of an item in a collection. /// Gets the index of an item in a collection.
/// </summary> /// </summary>
@ -409,85 +446,54 @@ namespace Perspex.Controls.Primitives
} }
/// <summary> /// <summary>
/// Generates a range of integers between the first and last inclusive. /// Gets a range of items from an IEnumerable.
/// </summary> /// </summary>
/// <param name="first">The first integer.</param> /// <param name="items">The items.</param>
/// <param name="last">The last integer.</param> /// <param name="first">The index of the first item.</param>
/// <returns>The range.</returns> /// <param name="last">The index of the last item.</param>
private static IEnumerable<int> Range(int first, int last) /// <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; int step = first > last ? -1 : 1;
for (int i = first; i != last; i += step) for (int i = first; i != last; i += step)
{ {
yield return i; yield return list[i];
} }
yield return last; yield return list[last];
} }
/// <summary> /// <summary>
/// Makes a list of integers equal the range first...last. /// Makes a list of objects equal another.
/// </summary> /// </summary>
/// <param name="indexes">The list of indexes.</param> /// <param name="items">The items collection.</param>
/// <param name="first">The first in the range.</param> /// <param name="desired">The desired items.</param>
/// <param name="last">The last in the range.</param> private static void SynchronizeItems(IList<object> items, IEnumerable<object> desired)
private static void SynchronizeIndexes(IPerspexList<int> indexes, int first, int last)
{ {
var i = 0; int index = 0;
var next = first;
int step = first > last ? -1 : 1;
while (i < indexes.Count && indexes[i] == next && next != last)
{
++i;
next += step;
}
if (next != last || i != indexes.Count - 1)
{
if (i < indexes.Count - 1)
{
indexes.RemoveRange(i, indexes.Count - i);
}
indexes.AddRange(Range(next, last)); foreach (var i in desired)
}
}
/// <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; if (index < items.Count)
var styleable = container as IStyleable;
_ignoreContainerSelectionChanged = true;
if (selectable != null)
{ {
selectable.IsSelected = selected; if (items[index] != i)
}
else if (styleable != null)
{
if (selected)
{ {
styleable.Classes.Add(":selected"); items[index] = i;
}
else
{
styleable.Classes.Remove(":selected");
} }
} }
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> /// <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> /// </summary>
/// <param name="index">The index of the item.</param> private void LostSelection()
/// <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); 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> /// <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> /// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param> /// <param name="container">The container.</param>
/// <param name="itemIndexes">The item indexes.</param> /// <param name="selected">Whether the control is selected</param>
private void SelectedIndexesAdded(int listIndex, IEnumerable<int> itemIndexes) private void MarkContainerSelected(IControl container, bool selected)
{ {
var indexes = (itemIndexes as IList<int>) ?? itemIndexes.ToList(); try
IControl container = null;
if (SelectedItems.Count != SelectedIndexes.Count)
{ {
var items = indexes.Select(x => Items.Cast<object>().ElementAt(x)); var selectable = container as ISelectable;
SelectedItems.AddRange(items); var styleable = container as IStyleable;
}
foreach (var itemIndex in indexes) _ignoreContainerSelectionChanged = true;
{
container = MarkIndexSelected(itemIndex, true);
}
if (SelectedIndexes.Count == 1) if (selectable != null)
{ {
RaisePropertyChanged(SelectedIndexProperty, -1, SelectedIndexes[0], BindingPriority.LocalValue); selectable.IsSelected = selected;
}
else if (styleable != null)
{
if (selected)
{
styleable.Classes.Add(":selected");
}
else
{
styleable.Classes.Remove(":selected");
}
}
} }
finally
if (container != null && Presenter?.Panel != null)
{ {
KeyboardNavigation.SetTabOnceActiveElement((InputElement)Presenter.Panel, container); _ignoreContainerSelectionChanged = false;
} }
} }
/// <summary> /// <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> /// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param> /// <param name="index">The index of the item.</param>
/// <param name="itemIndexes">The item indexes.</param> /// <param name="selected">Whether the item should be selected or deselected.</param>
private void SelectedIndexesRemoved(int listIndex, IEnumerable<int> itemIndexes) private void MarkItemSelected(int index, bool selected)
{ {
var sync = SelectedIndexes.Count != SelectedItems.Count; var container = ItemContainerGenerator.ContainerFromIndex(index);
SelectedItems.RemoveRange(listIndex, itemIndexes.Count());
foreach (var itemIndex in itemIndexes)
{
MarkIndexSelected(itemIndex, false);
}
if (SelectedIndexes.Count == 0) if (container != null)
{ {
RaisePropertyChanged( MarkContainerSelected(container, selected);
SelectedIndexProperty,
itemIndexes.First(),
-1,
BindingPriority.LocalValue);
} }
} }
/// <summary> /// <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> /// </summary>
/// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</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) var index = IndexOf(Items, item);
{
SelectedIndexes.Insert(index, IndexOf(Items, item));
}
if (SelectedItems.Count == 1) if (index != -1)
{ {
RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue); MarkItemSelected(index, selected);
} }
} }
/// <summary> /// <summary>
/// Called when an item is removed from the <see cref="SelectedItems"/> collection. /// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
/// </summary> /// </summary>
/// <param name="index">The index in the SelectedItems collection.</param> /// <param name="sender">The event sender.</param>
/// <param name="item">The item.</param> /// <param name="e">The event args.</param>
private void SelectedItemsRemoved(int index, object item) 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> /// <summary>
/// Called when the <see cref="SelectedItems"/> collection is reset. /// Called when items are added to the <see cref="SelectedItems"/> collection.
/// </summary> /// </summary>
private void SelectionReset() /// <param name="items">The added items.</param>
private void SelectedItemsAdded(IList<object> items)
{ {
if (SelectedIndexes.Count > 0) if (items.Count > 0)
{ {
SelectedIndexes.Clear(); foreach (var item in items)
} {
MarkItemSelected(item, true);
}
if (SelectedItems.Count > 0) if (SelectedItem == null)
{ {
SelectedItems.Clear(); var index = IndexOf(Items, items[0]);
}
foreach (var container in ItemContainerGenerator.Containers) if (index != -1)
{ {
MarkContainerSelected(container, false); _selectedItem = items[0];
_selectedIndex = index;
RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue);
}
}
} }
} }
/// <summary> /// <summary>
/// Validates items added to the <see cref="SelectedIndexes"/> collection. /// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary> /// </summary>
/// <param name="index">The index to be added.</param> private void SubscribeToSelectedItems()
private void ValidateIndex(int index)
{ {
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> /// <summary>
/// Called when the currently selected item is lost and the selection must be changed /// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// depending on the <see cref="SelectionMode"/> property.
/// </summary> /// </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); incc.CollectionChanged -= SelectedItemsCollectionChanged;
if (index > -1)
{
SelectedItem = items.ElementAt(index);
return;
}
} }
SelectedIndex = -1;
} }
} }
} }

13
src/Perspex.Controls/TabControl.cs

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

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

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using Perspex.Controls.Presenters; using Perspex.Controls.Presenters;
using Perspex.Controls.Templates; using Perspex.Controls.Templates;
using Perspex.Input;
using Perspex.LogicalTree; using Perspex.LogicalTree;
using Perspex.Styling; using Perspex.Styling;
using Xunit; using Xunit;
@ -65,6 +66,30 @@ namespace Perspex.Controls.UnitTests
dataContexts); 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) private Control CreateListBoxTemplate(ITemplatedControl parent)
{ {
return new ScrollViewer 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]); 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() private ControlTemplate Template()
{ {
return new ControlTemplate<SelectingItemsControl>(control => return new ControlTemplate<SelectingItemsControl>(control =>

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

@ -1,6 +1,8 @@
// Copyright (c) The Perspex Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Perspex.Collections; using Perspex.Collections;
using Perspex.Controls.Presenters; using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives; using Perspex.Controls.Primitives;
@ -12,7 +14,7 @@ namespace Perspex.Controls.UnitTests.Primitives
public class SelectingItemsControlTests_Multiple public class SelectingItemsControlTests_Multiple
{ {
[Fact] [Fact]
public void Setting_SelectedIndex_Should_Add_To_SelectedIndexes() public void Setting_SelectedIndex_Should_Add_To_SelectedItems()
{ {
var target = new TestSelector var target = new TestSelector
{ {
@ -23,11 +25,11 @@ namespace Perspex.Controls.UnitTests.Primitives
target.ApplyTemplate(); target.ApplyTemplate();
target.SelectedIndex = 1; target.SelectedIndex = 1;
Assert.Equal(new[] { 1 }, target.SelectedIndexes); Assert.Equal(new[] { "bar" }, target.SelectedItems.ToList());
} }
[Fact] [Fact]
public void Adding_SelectedIndexes_Should_Set_SelectedIndex() public void Adding_SelectedItems_Should_Set_SelectedIndex()
{ {
var target = new TestSelector var target = new TestSelector
{ {
@ -36,13 +38,13 @@ namespace Perspex.Controls.UnitTests.Primitives
}; };
target.ApplyTemplate(); target.ApplyTemplate();
target.SelectedIndexes.Add(1); target.SelectedItems.Add("bar");
Assert.Equal(1, target.SelectedIndex); Assert.Equal(1, target.SelectedIndex);
} }
[Fact] [Fact]
public void Adding_First_SelectedIndex_Should_Raise_SelectedIndex_SelectedItem_Changed() public void Assigning_SelectedItems_Should_Set_SelectedIndex()
{ {
var target = new TestSelector var target = new TestSelector
{ {
@ -50,27 +52,14 @@ namespace Perspex.Controls.UnitTests.Primitives
Template = Template(), 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.ApplyTemplate();
target.SelectedIndexes.Add(1); target.SelectedItems = new[] { "bar" };
Assert.True(indexRaised); Assert.Equal(1, target.SelectedIndex);
Assert.True(itemRaised);
} }
[Fact] [Fact]
public void Adding_Subsequent_SelectedIndexes_Should_Not_Raise_SelectedIndex_SelectedItem_Changed() public void Reassigning_SelectedItems_Should_Clear_Selection()
{ {
var target = new TestSelector var target = new TestSelector
{ {
@ -79,16 +68,11 @@ namespace Perspex.Controls.UnitTests.Primitives
}; };
target.ApplyTemplate(); target.ApplyTemplate();
target.SelectedIndexes.Add(0); target.SelectedItems.Add("bar");
target.SelectedItems = new PerspexList<object>();
bool raised = false;
target.PropertyChanged += (s, e) =>
raised |= e.Property.Name == "SelectedIndex" ||
e.Property.Name == "SelectedItem";
target.SelectedIndexes.Add(1);
Assert.False(raised); Assert.Equal(-1, target.SelectedIndex);
Assert.Equal(null, target.SelectedItem);
} }
[Fact] [Fact]
@ -120,7 +104,7 @@ namespace Perspex.Controls.UnitTests.Primitives
} }
[Fact] [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 var target = new TestSelector
{ {
@ -129,94 +113,168 @@ namespace Perspex.Controls.UnitTests.Primitives
}; };
target.ApplyTemplate(); target.ApplyTemplate();
target.SelectedIndexes.Add(0); target.SelectedItems.Add("foo");
bool raised = false; bool raised = false;
target.PropertyChanged += (s, e) => target.PropertyChanged += (s, e) =>
raised = e.Property.Name == "SelectedIndex" && raised |= e.Property.Name == "SelectedIndex" ||
(int)e.OldValue == 0 && e.Property.Name == "SelectedItem";
(int)e.NewValue == -1;
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); Assert.True(raised);
} }
[Fact] [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 var target = new TestSelector
{ {
Items = new[] Items = items,
{
"foo",
"bar",
},
Template = Template(), Template = Template(),
}; };
target.ApplyTemplate(); 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] [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 var target = new TestSelector
{ {
Items = new[] Items = items,
{
"foo",
"bar",
},
Template = Template(), Template = Template(),
}; };
target.ApplyTemplate(); 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] [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 var target = new TestSelector
{ {
Items = new[] Items = items,
{
new ListBoxItem(),
new ListBoxItem(),
},
Template = Template(), Template = Template(),
}; };
target.ApplyTemplate(); 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] [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 var target = new TestSelector
{ {
Items = new[] Items = items,
{
new ListBoxItem(),
new ListBoxItem(),
},
Template = Template(), Template = Template(),
}; };
target.ApplyTemplate(); target.ApplyTemplate();
target.SelectedIndexes.Add(1); target.SelectedIndex = 1;
target.SelectedIndexes.Remove(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] [Fact]
@ -241,8 +299,7 @@ namespace Perspex.Controls.UnitTests.Primitives
target.SelectedIndex = 1; target.SelectedIndex = 1;
target.SelectRange(3); target.SelectRange(3);
Assert.Equal(new[] { 1, 2, 3 }, target.SelectedIndexes); Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems.ToList());
Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems);
} }
[Fact] [Fact]
@ -267,8 +324,7 @@ namespace Perspex.Controls.UnitTests.Primitives
target.SelectedIndex = 3; target.SelectedIndex = 3;
target.SelectRange(1); target.SelectRange(1);
Assert.Equal(new[] { 3, 2, 1 }, target.SelectedIndexes); Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems.ToList());
Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems);
} }
[Fact] [Fact]
@ -294,20 +350,15 @@ namespace Perspex.Controls.UnitTests.Primitives
target.SelectRange(5); target.SelectRange(5);
target.SelectRange(4); target.SelectRange(4);
Assert.Equal(new[] { 2, 3, 4 }, target.SelectedIndexes); Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.ToList());
Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems);
} }
private class TestSelector : SelectingItemsControl private class TestSelector : SelectingItemsControl
{ {
public new IPerspexList<int> SelectedIndexes public new IList<object> SelectedItems
{
get { return base.SelectedIndexes; }
}
public new IPerspexList<object> SelectedItems
{ {
get { return base.SelectedItems; } get { return base.SelectedItems; }
set { base.SelectedItems = value; }
} }
public new SelectionMode SelectionMode public new SelectionMode SelectionMode

Loading…
Cancel
Save