Browse Source

Use SelectionModel in SelectingItemsControl.

fixes/tree-selectionmodel
Steven Kirk 7 years ago
parent
commit
6001115573
  1. 4
      samples/BindingDemo/MainWindow.xaml
  2. 5
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  3. 2
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  4. 2
      samples/VirtualizationDemo/MainWindow.xaml
  5. 14
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  6. 4
      src/Avalonia.Controls/ComboBox.cs
  7. 19
      src/Avalonia.Controls/ListBox.cs
  8. 834
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  9. 3
      src/Avalonia.Controls/SelectionModel.cs
  10. 5
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  11. 6
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  12. 31
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  13. 4
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
  14. 211
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  15. 9
      tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs
  16. 6
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  17. 13
      tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

4
samples/BindingDemo/MainWindow.xaml

@ -74,11 +74,11 @@
</StackPanel.DataTemplates>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel>
<ContentControl Content="{Binding SelectedItems[0]}">
<ContentControl.DataTemplates>

5
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

@ -6,6 +6,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Threading;
using ReactiveUI;
using Avalonia.Controls;
namespace BindingDemo.ViewModels
{
@ -27,7 +28,7 @@ namespace BindingDemo.ViewModels
Detail = "Item " + x + " details",
}));
SelectedItems = new ObservableCollection<TestItem>();
Selection = new SelectionModel();
ShuffleItems = ReactiveCommand.Create(() =>
{
@ -56,7 +57,7 @@ namespace BindingDemo.ViewModels
}
public ObservableCollection<TestItem> Items { get; }
public ObservableCollection<TestItem> SelectedItems { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> ShuffleItems { get; }
public string BooleanString

2
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -10,7 +10,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<Button Command="{Binding AddItemCommand}">Add</Button>

2
samples/VirtualizationDemo/MainWindow.xaml

@ -45,7 +45,7 @@
<ListBox Name="listBox"
Items="{Binding Items}"
SelectedItems="{Binding SelectedItems}"
Selection="{Binding Selection}"
SelectionMode="Multiple"
VirtualizationMode="{Binding VirtualizationMode}"
ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"

14
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -51,8 +51,7 @@ namespace VirtualizationDemo.ViewModels
set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
}
public AvaloniaList<ItemViewModel> SelectedItems { get; }
= new AvaloniaList<ItemViewModel>();
public SelectionModel Selection { get; } = new SelectionModel();
public AvaloniaList<ItemViewModel> Items
{
@ -141,9 +140,9 @@ namespace VirtualizationDemo.ViewModels
{
var index = Items.Count;
if (SelectedItems.Count > 0)
if (Selection.SelectedIndices.Count > 0)
{
index = Items.IndexOf(SelectedItems[0]);
index = Selection.SelectedIndex.GetAt(0);
}
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
@ -151,9 +150,9 @@ namespace VirtualizationDemo.ViewModels
private void Remove()
{
if (SelectedItems.Count > 0)
if (Selection.SelectedItems.Count > 0)
{
Items.RemoveAll(SelectedItems);
Items.RemoveAll(Selection.SelectedItems.Cast<ItemViewModel>().ToList());
}
}
@ -167,8 +166,7 @@ namespace VirtualizationDemo.ViewModels
private void SelectItem(int index)
{
SelectedItems.Clear();
SelectedItems.Add(Items[index]);
Selection.SelectedIndex = new IndexPath(index);
}
}
}

4
src/Avalonia.Controls/ComboBox.cs

@ -289,9 +289,9 @@ namespace Avalonia.Controls
{
var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
if (container == null && SelectedItems.Count > 0)
if (container == null && SelectedIndex != -1)
{
ScrollIntoView(SelectedItems[0]);
ScrollIntoView(Selection.SelectedItem);
container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
}

19
src/Avalonia.Controls/ListBox.cs

@ -34,6 +34,12 @@ namespace Avalonia.Controls
public static readonly new DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty =
SelectingItemsControl.SelectedItemsProperty;
/// <summary>
/// Defines the <see cref="Selection"/> property.
/// </summary>
public static readonly new DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
SelectingItemsControl.SelectionProperty;
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
@ -73,6 +79,15 @@ namespace Avalonia.Controls
set => base.SelectedItems = value;
}
/// <summary>
/// Gets or sets a model holding the current selection.
/// </summary>
public new ISelectionModel Selection
{
get => base.Selection;
set => base.Selection = value;
}
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
@ -98,12 +113,12 @@ namespace Avalonia.Controls
/// <summary>
/// Selects all items in the <see cref="ListBox"/>.
/// </summary>
public new void SelectAll() => base.SelectAll();
public void SelectAll() => Selection.SelectAll();
/// <summary>
/// Deselects all items in the <see cref="ListBox"/>.
/// </summary>
public new void UnselectAll() => base.UnselectAll();
public void UnselectAll() => Selection.ClearSelection();
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()

834
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

File diff suppressed because it is too large

3
src/Avalonia.Controls/SelectionModel.cs

@ -176,11 +176,12 @@ namespace Avalonia.Controls
{
var isSelected = IsSelectedWithPartialAt(value);
if (!isSelected.HasValue || !isSelected.Value)
if (!IsSelectedAt(value) || SelectedItems.Count > 1)
{
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
SelectWithPathImpl(value, select: true);
ApplyAutoSelect();
}
}
}

5
src/Avalonia.Controls/Utils/SelectedItemsSync.cs

@ -19,6 +19,7 @@ namespace Avalonia.Controls.Utils
public SelectedItemsSync(ISelectionModel model)
{
model = model ?? throw new ArgumentNullException(nameof(model));
Model = model;
}
@ -37,9 +38,9 @@ namespace Avalonia.Controls.Utils
return _items;
}
public void SetItems(IList items)
public void SetItems(IList? items)
{
items = items ?? throw new ArgumentNullException(nameof(items));
items ??= new AvaloniaList<object>();
if (items.IsFixedSize)
{

6
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -275,7 +275,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Selected_Item_Changes_To_NextAvailable_Item_If_SelectedItem_Is_Removed_From_Middle()
public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle()
{
var items = new ObservableCollection<string>
{
@ -298,8 +298,8 @@ namespace Avalonia.Controls.UnitTests
items.RemoveAt(1);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("FooBar", target.SelectedItem);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("Foo", target.SelectedItem);
}
private Control CreateTemplate(Carousel control, INameScope scope)

31
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -554,33 +554,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { removed }, receivedArgs.RemovedItems);
}
[Fact]
public void Moving_Selected_Item_Should_Update_Selection()
{
var items = new AvaloniaList<Item>
{
new Item(),
new Item(),
};
var target = new SelectingItemsControl
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndex = 0;
Assert.Equal(items[0], target.SelectedItem);
Assert.Equal(0, target.SelectedIndex);
items.Move(0, 1);
Assert.Equal(items[1], target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Resetting_Items_Collection_Should_Clear_Selection()
{
@ -1101,8 +1074,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
items[1] = "Qux";
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Qux", target.SelectedItem);
Assert.Equal(-1, target.SelectedIndex);
Assert.Null(target.SelectedItem);
}
[Fact]

4
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs

@ -78,8 +78,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.SelectedIndex = 2;
items.RemoveAt(2);
Assert.Equal(2, target.SelectedIndex);
Assert.Equal("qux", target.SelectedItem);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("foo", target.SelectedItem);
}
[Fact]

211
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -73,8 +73,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
[Fact]
public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex()
{
// Note that we don't need SelectionMode = Multiple here. Multiple selections can always
// be made in code.
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz" },
@ -340,7 +338,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
"qiz",
"lol",
},
SelectionMode = SelectionMode.Multiple,
Template = Template(),
};
@ -373,7 +370,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.SelectedIndex = 3;
target.SelectRange(1);
Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems.Cast<object>().ToList());
Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems.Cast<object>().ToList());
}
[Fact]
@ -1117,7 +1114,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.SelectAll();
items[1] = "Qux";
Assert.Equal(new[] { "Foo", "Qux", "Baz" }, target.SelectedItems);
Assert.Equal(new[] { "Foo", "Baz" }, target.SelectedItems);
}
[Fact]
@ -1255,6 +1252,195 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(1, target.SelectedItems.Count);
}
[Fact]
public void Adding_To_Selection_Should_Set_SelectedIndex()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.Selection.Select(1);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Assigning_Null_To_Selection_Should_Create_New_SelectionModel()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
var oldSelection = target.Selection;
target.Selection = null;
Assert.NotNull(target.Selection);
Assert.NotSame(oldSelection, target.Selection);
}
[Fact]
public void Assigning_SelectionModel_With_Different_Source_To_Selection_Should_Fail()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
var selection = new SelectionModel { Source = new[] { "baz" } };
Assert.Throws<ArgumentException>(() => target.Selection = selection);
}
[Fact]
public void Assigning_SelectionModel_With_Null_Source_To_Selection_Should_Set_Source()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
var selection = new SelectionModel();
target.Selection = selection;
Assert.Same(target.Items, selection.Source);
}
[Fact]
public void Assigning_Single_Selected_Item_To_Selection_Should_Set_SelectedIndex()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var selection = new SelectionModel { Source = target.Items };
selection.Select(1);
target.Selection = selection;
Assert.Equal(1, target.SelectedIndex);
Assert.Equal(new[] { "bar" }, target.Selection.SelectedItems);
Assert.Equal(new[] { 1 }, SelectedContainers(target));
}
[Fact]
public void Assigning_Multiple_Selected_Items_To_Selection_Should_Set_SelectedIndex()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz" },
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var selection = new SelectionModel { Source = target.Items };
selection.SelectRange(new IndexPath(0), new IndexPath(2));
target.Selection = selection;
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { "foo", "bar", "baz" }, target.Selection.SelectedItems);
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
}
[Fact]
public void Reassigning_Selection_Should_Clear_Selection()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.Selection.Select(1);
target.Selection = new SelectionModel();
Assert.Equal(-1, target.SelectedIndex);
Assert.Null(target.SelectedItem);
}
[Fact]
public void Assigning_Selection_Should_Set_Item_IsSelected()
{
var items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
new ListBoxItem(),
};
var target = new TestSelector
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var selection = new SelectionModel { Source = items };
selection.SelectRange(new IndexPath(0), new IndexPath(1));
target.Selection = selection;
Assert.True(items[0].IsSelected);
Assert.True(items[1].IsSelected);
Assert.False(items[2].IsSelected);
}
[Fact]
public void Assigning_Selection_Should_Raise_SelectionChanged()
{
var items = new[] { "foo", "bar", "baz" };
var target = new TestSelector
{
Items = items,
Template = Template(),
SelectedItem = "bar",
};
var raised = 0;
target.SelectionChanged += (s, e) =>
{
if (raised == 0)
{
Assert.Empty(e.AddedItems.Cast<object>());
Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast<object>());
}
else
{
Assert.Equal(new[] { "foo", "baz" }, e.AddedItems.Cast<object>());
Assert.Empty(e.RemovedItems.Cast<object>());
}
++raised;
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var selection = new SelectionModel { Source = items };
selection.Select(0);
selection.Select(2);
target.Selection = selection;
Assert.Equal(2, raised);
}
private IEnumerable<int> SelectedContainers(SelectingItemsControl target)
{
return target.Presenter.Panel.Children
@ -1278,20 +1464,31 @@ namespace Avalonia.Controls.UnitTests.Primitives
public static readonly new AvaloniaProperty<IList> SelectedItemsProperty =
SelectingItemsControl.SelectedItemsProperty;
public TestSelector()
{
SelectionMode = SelectionMode.Multiple;
}
public new IList SelectedItems
{
get { return base.SelectedItems; }
set { base.SelectedItems = value; }
}
public new ISelectionModel Selection
{
get => base.Selection;
set => base.Selection = value;
}
public new SelectionMode SelectionMode
{
get { return base.SelectionMode; }
set { base.SelectionMode = value; }
}
public new void SelectAll() => base.SelectAll();
public new void UnselectAll() => base.UnselectAll();
public void SelectAll() => Selection.SelectAll();
public void UnselectAll() => Selection.ClearSelection();
public void SelectRange(int index) => UpdateSelection(index, true, true);
public void Toggle(int index) => UpdateSelection(index, true, false, true);
}

9
tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs

@ -70,7 +70,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void Removing_Selected_Should_Select_Next()
public void Removing_Selected_Should_Select_First()
{
var items = new ObservableCollection<TabItem>()
{
@ -99,10 +99,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Same(items[1], target.SelectedItem);
items.RemoveAt(1);
// Assert for former element [2] now [1] == "3rd"
Assert.Equal(1, target.SelectedIndex);
Assert.Same(items[1], target.SelectedItem);
Assert.Same("3rd", ((TabItem)target.SelectedItem).Name);
Assert.Equal(0, target.SelectedIndex);
Assert.Same(items[0], target.SelectedItem);
Assert.Same("first", ((TabItem)target.SelectedItem).Name);
}
private Control CreateTabStripTemplate(TabStrip parent, INameScope scope)

6
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -95,7 +95,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Removal_Should_Set_Next_Tab()
public void Removal_Should_Set_First_Tab()
{
var collection = new ObservableCollection<TabItem>()
{
@ -126,11 +126,9 @@ namespace Avalonia.Controls.UnitTests
target.SelectedItem = collection[1];
collection.RemoveAt(1);
// compare with former [2] now [1] == "3rd"
Assert.Same(collection[1], target.SelectedItem);
Assert.Same(collection[0], target.SelectedItem);
}
[Fact]
public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate()
{

13
tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

@ -169,6 +169,19 @@ namespace Avalonia.Controls.UnitTests.Utils
Assert.Equal(new[] { "foo", "baz", "bar" }, items);
}
[Fact]
public void Setting_Items_To_Null_Creates_Empty_Items()
{
var target = CreateTarget();
var oldItems = target.GetOrCreateItems();
target.SetItems(null);
var newItems = Assert.IsType<AvaloniaList<object>>(target.GetOrCreateItems());
Assert.NotSame(oldItems, newItems);
}
[Fact]
public void Handles_Null_Model_Source()
{

Loading…
Cancel
Save