Browse Source

Started implementing multiple selection.

Steven Kirk 10 years ago
parent
commit
b2d40e77c3

+ 1 - 1
samples/XamlTestApplicationPcl/Views/MainWindow.paml

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

+ 69 - 13
src/Perspex.Base/Collections/PerspexListExtensions.cs

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

+ 5 - 0
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

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

+ 5 - 0
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

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

+ 5 - 0
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

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

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

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

+ 237 - 95
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

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

+ 23 - 0
src/Perspex.Controls/Primitives/TabStrip.cs

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

+ 0 - 19
tests/Perspex.Controls.UnitTests/ListBoxTests.cs

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

+ 188 - 0
tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs

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

+ 2 - 0
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

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

+ 25 - 70
tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

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

+ 246 - 0
tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

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