Browse Source
To sync between an `ISelectionModel` and a `SelectedItems` collection.fixes/tree-selectionmodel
2 changed files with 436 additions and 0 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue