Browse Source

Added SelectedItemsSync.

To sync between an `ISelectionModel` and a `SelectedItems` collection.
fixes/tree-selectionmodel
Steven Kirk 6 years ago
parent
commit
0c894e20d3
  1. 226
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  2. 210
      tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

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

@ -0,0 +1,226 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Controls.Utils
{
/// <summary>
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
/// </summary>
internal class SelectedItemsSync
{
private IList? _items;
private bool _updatingItems;
private bool _updatingModel;
public SelectedItemsSync(ISelectionModel model)
{
Model = model;
}
public ISelectionModel Model { get; private set; }
public IList GetOrCreateItems()
{
if (_items == null)
{
var items = new AvaloniaList<object>(Model.SelectedItems);
items.CollectionChanged += ItemsCollectionChanged;
Model.SelectionChanged += SelectionModelSelectionChanged;
_items = items;
}
return _items;
}
public void SetItems(IList items)
{
items = items ?? throw new ArgumentNullException(nameof(items));
if (items.IsFixedSize)
{
throw new NotSupportedException(
"Cannot assign fixed size selection to SelectedItems.");
}
if (_items is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= ItemsCollectionChanged;
}
if (_items == null)
{
Model.SelectionChanged += SelectionModelSelectionChanged;
}
try
{
_updatingModel = true;
_items = items;
using (Model.Update())
{
Model.ClearSelection();
Add(items);
}
if (_items is INotifyCollectionChanged incc2)
{
incc2.CollectionChanged += ItemsCollectionChanged;
}
}
finally
{
_updatingModel = false;
}
}
public void SetModel(ISelectionModel model)
{
model = model ?? throw new ArgumentNullException(nameof(model));
if (_items != null)
{
Model.SelectionChanged -= SelectionModelSelectionChanged;
Model = model;
Model.SelectionChanged += SelectionModelSelectionChanged;
try
{
_updatingItems = true;
_items.Clear();
foreach (var i in model.SelectedItems)
{
_items.Add(i);
}
}
finally
{
_updatingItems = false;
}
}
}
private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_updatingItems)
{
return;
}
if (_items == null)
{
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
}
void Remove()
{
foreach (var i in e.OldItems)
{
var index = IndexOf(Model.Source, i);
if (index != -1)
{
Model.Deselect(index);
}
}
}
try
{
using var operation = Model.Update();
_updatingModel = true;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
Remove();
break;
case NotifyCollectionChangedAction.Replace:
Remove();
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
Model.ClearSelection();
Add(_items);
break;
}
}
finally
{
_updatingModel = false;
}
}
private void Add(IList newItems)
{
foreach (var i in newItems)
{
var index = IndexOf(Model.Source, i);
if (index != -1)
{
Model.Select(index);
}
}
}
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_updatingModel)
{
return;
}
if (_items == null)
{
throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items.");
}
try
{
var deselected = e.DeselectedItems.ToList();
var selected = e.SelectedItems.ToList();
_updatingItems = true;
foreach (var i in deselected)
{
_items.Remove(i);
}
foreach (var i in selected)
{
_items.Add(i);
}
}
finally
{
_updatingItems = false;
}
}
private static int IndexOf(object source, object item)
{
if (source is IList l)
{
return l.IndexOf(item);
}
else if (source is ItemsSourceView v)
{
return v.IndexOf(item);
}
return -1;
}
}
}

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

@ -0,0 +1,210 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Controls.Utils;
using Xunit;
namespace Avalonia.Controls.UnitTests.Utils
{
public class SelectedItemsSyncTests
{
[Fact]
public void Initial_Items_Are_From_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
Assert.Equal(new[] { "bar", "baz" }, items);
}
[Fact]
public void Selecting_On_Model_Adds_Item()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
target.Model.Select(0);
Assert.Equal(new[] { "bar", "baz", "foo" }, items);
}
[Fact]
public void Selecting_Duplicate_On_Model_Adds_Item()
{
var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
var items = target.GetOrCreateItems();
target.Model.Select(4);
Assert.Equal(new[] { "bar", "baz", "bar" }, items);
}
[Fact]
public void Deselecting_On_Model_Removes_Item()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
target.Model.Deselect(1);
Assert.Equal(new[] { "baz" }, items);
}
[Fact]
public void Deselecting_Duplicate_On_Model_Removes_Item()
{
var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
var items = target.GetOrCreateItems();
target.Model.Select(4);
target.Model.Deselect(4);
Assert.Equal(new[] { "baz", "bar" }, items);
}
[Fact]
public void Reassigning_Model_Resets_Items()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var newModel = new SelectionModel { Source = target.Model.Source };
newModel.Select(0);
newModel.Select(1);
target.SetModel(newModel);
Assert.Equal(new[] { "foo", "bar" }, items);
}
[Fact]
public void Reassigning_Model_Tracks_New_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var newModel = new SelectionModel { Source = target.Model.Source };
target.SetModel(newModel);
newModel.Select(0);
newModel.Select(1);
Assert.Equal(new[] { "foo", "bar" }, items);
}
[Fact]
public void Adding_To_Items_Selects_On_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
items.Add("foo");
Assert.Equal(
new[] { new IndexPath(0), new IndexPath(1), new IndexPath(2) },
target.Model.SelectedIndices);
Assert.Equal(new[] { "bar", "baz", "foo" }, items);
}
[Fact]
public void Removing_From_Items_Deselects_On_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
items.Remove("baz");
Assert.Equal(new[] { new IndexPath(1) }, target.Model.SelectedIndices);
Assert.Equal(new[] { "bar" }, items);
}
[Fact]
public void Replacing_Item_Updates_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
items[0] = "foo";
Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices);
Assert.Equal(new[] { "foo", "baz" }, items);
}
[Fact]
public void Clearing_Items_Updates_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
items.Clear();
Assert.Empty(target.Model.SelectedIndices);
}
[Fact]
public void Setting_Items_Updates_Model()
{
var target = CreateTarget();
var oldItems = target.GetOrCreateItems();
var newItems = new AvaloniaList<string> { "foo", "baz" };
target.SetItems(newItems);
Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices);
Assert.Same(newItems, target.GetOrCreateItems());
Assert.NotSame(oldItems, target.GetOrCreateItems());
Assert.Equal(new[] { "foo", "baz" }, newItems);
}
[Fact]
public void Setting_Items_Subscribes_To_Model()
{
var target = CreateTarget();
var items = new AvaloniaList<string> { "foo", "baz" };
target.SetItems(items);
target.Model.Select(1);
Assert.Equal(new[] { "foo", "baz", "bar" }, items);
}
[Fact]
public void Handles_Null_Model_Source()
{
var model = new SelectionModel();
model.Select(1);
var target = new SelectedItemsSync(model);
var items = target.GetOrCreateItems();
Assert.Empty(items);
model.Select(2);
model.Source = new[] { "foo", "bar", "baz" };
Assert.Equal(new[] { "bar", "baz" }, items);
}
[Fact]
public void Does_Not_Accept_Fixed_Size_Items()
{
var target = CreateTarget();
Assert.Throws<NotSupportedException>(() =>
target.SetItems(new[] { "foo", "bar", "baz" }));
}
private static SelectedItemsSync CreateTarget(
IEnumerable<string> items = null)
{
items ??= new[] { "foo", "bar", "baz" };
var model = new SelectionModel { Source = items };
model.SelectRange(new IndexPath(1), new IndexPath(2));
var target = new SelectedItemsSync(model);
return target;
}
}
}
Loading…
Cancel
Save