Ver Fonte

Make SelectedItems bindable.

And to avoid confusion removed SelectedIndexes.
Steven Kirk há 10 anos atrás
pai
commit
c0dff57a78

+ 43 - 21
samples/BindingTest/MainWindow.paml

@@ -1,22 +1,44 @@
-<Window xmlns="https://github.com/perspex">
-  <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}"/>
-    </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>
-    </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>
+<Window xmlns="https://github.com/perspex"
+        xmlns:vm="clr-namespace:BindingTest.ViewModels;assembly=BindingTest">
+  <TabControl>
+    <TabItem Header="Basic">
+      <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 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 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 BooleanString}"/>
+          <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>

+ 9 - 6
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.ObjectModel;
+using System.Linq;
 using ReactiveUI;
 
 namespace BindingTest.ViewModels
@@ -11,12 +12,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<int> { 4, 6 };
 
             ShuffleItems = ReactiveCommand.Create();
             ShuffleItems.Subscribe(_ =>
@@ -27,6 +29,7 @@ namespace BindingTest.ViewModels
         }
 
         public ObservableCollection<TestItem> Items { get; }
+        public ObservableCollection<int> SelectedItems { get; }
         public ReactiveCommand<object> ShuffleItems { get; }
 
         public string BooleanString

+ 14 - 0
src/Perspex.Controls/ListBox.cs

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

+ 255 - 200
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<object>> SelectedItemsProperty =
+            PerspexProperty.RegisterDirect<SelectingItemsControl, IList<object>>(
                 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<object> _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
+                    {
+                        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<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>
@@ -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<object> items, IEnumerable<object> desired)
         {
-            var i = 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);
-                }
+            int index = 0;
 
-                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
+            foreach (var i in desired)
             {
-                var selectable = container as ISelectable;
-                var styleable = container as IStyleable;
-
-                _ignoreContainerSelectionChanged = true;
-
-                if (selectable != null)
+                if (index < items.Count)
                 {
-                    selectable.IsSelected = selected;
-                }
-                else if (styleable != null)
-                {
-                    if (selected)
+                    if (items[index] != i)
                     {
-                        styleable.Classes.Add(":selected");
-                    }
-                    else
-                    {
-                        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<object> 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;
         }
     }
 }

+ 5 - 8
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 - 0
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

+ 0 - 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 =>

+ 132 - 81
tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@@ -1,6 +1,8 @@
 // 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.Generic;
+using System.Linq;
 using Perspex.Collections;
 using Perspex.Controls.Presenters;
 using Perspex.Controls.Primitives;
@@ -12,7 +14,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 +25,11 @@ namespace Perspex.Controls.UnitTests.Primitives
             target.ApplyTemplate();
             target.SelectedIndex = 1;
 
-            Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+            Assert.Equal(new[] { "bar" }, target.SelectedItems.ToList());
         }
 
         [Fact]
-        public void Adding_SelectedIndexes_Should_Set_SelectedIndex()
+        public void Adding_SelectedItems_Should_Set_SelectedIndex()
         {
             var target = new TestSelector
             {
@@ -36,13 +38,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 +52,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 +68,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 +104,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 +113,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 +299,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.ToList());
         }
 
         [Fact]
@@ -267,8 +324,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.ToList());
         }
 
         [Fact]
@@ -294,20 +350,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.ToList());
         }
 
         private class TestSelector : SelectingItemsControl
         {
-            public new IPerspexList<int> SelectedIndexes
-            {
-                get { return base.SelectedIndexes; }
-            }
-
-            public new IPerspexList<object> SelectedItems
+            public new IList<object> SelectedItems
             {
                 get { return base.SelectedItems; }
+                set { base.SelectedItems = value; }
             }
 
             public new SelectionMode SelectionMode