Browse Source
- Remove `SelectedItemsSync` and store `SelectedItems` in a new `InternalSelectionModel` - Store transient `SelectingItemsControl` state in an `UpdateState` object Fixes #4272pull/4659/head
6 changed files with 712 additions and 624 deletions
@ -0,0 +1,251 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Specialized; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Linq; |
|||
using Avalonia.Collections; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
internal class InternalSelectionModel : SelectionModel<object?> |
|||
{ |
|||
private IList? _selectedItems; |
|||
private bool _ignoreModelChanges; |
|||
private bool _ignoreSelectedItemsChanges; |
|||
|
|||
public InternalSelectionModel() |
|||
{ |
|||
SelectionChanged += OnSelectionChanged; |
|||
SourceReset += OnSourceReset; |
|||
} |
|||
|
|||
[AllowNull] |
|||
public new IList SelectedItems |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedItems is null) |
|||
{ |
|||
_selectedItems = new AvaloniaList<object?>(); |
|||
SubscribeToSelectedItems(); |
|||
} |
|||
|
|||
return _selectedItems; |
|||
} |
|||
set |
|||
{ |
|||
value ??= new AvaloniaList<object?>(); |
|||
|
|||
if (value.IsFixedSize) |
|||
{ |
|||
throw new NotSupportedException("Cannot assign fixed size selection to SelectedItems."); |
|||
} |
|||
|
|||
if (_selectedItems != value) |
|||
{ |
|||
UnsubscribeFromSelectedItems(); |
|||
_selectedItems = value; |
|||
SyncFromSelectedItems(); |
|||
SubscribeToSelectedItems(); |
|||
|
|||
if (ItemsView is null) |
|||
{ |
|||
SetInitSelectedItems(value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private protected override void SetSource(IEnumerable? value) |
|||
{ |
|||
try |
|||
{ |
|||
_ignoreSelectedItemsChanges = true; |
|||
base.SetSource(value); |
|||
} |
|||
finally |
|||
{ |
|||
_ignoreSelectedItemsChanges = false; |
|||
} |
|||
|
|||
SyncToSelectedItems(); |
|||
} |
|||
|
|||
private void SyncToSelectedItems() |
|||
{ |
|||
if (_selectedItems is object) |
|||
{ |
|||
try |
|||
{ |
|||
_ignoreSelectedItemsChanges = true; |
|||
_selectedItems.Clear(); |
|||
|
|||
foreach (var i in base.SelectedItems) |
|||
{ |
|||
_selectedItems.Add(i); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_ignoreSelectedItemsChanges = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void SyncFromSelectedItems() |
|||
{ |
|||
if (Source is null || _selectedItems is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
_ignoreModelChanges = true; |
|||
|
|||
using (BatchUpdate()) |
|||
{ |
|||
Clear(); |
|||
Add(_selectedItems); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_ignoreModelChanges = false; |
|||
} |
|||
} |
|||
|
|||
private void SubscribeToSelectedItems() |
|||
{ |
|||
if (_selectedItems is INotifyCollectionChanged incc) |
|||
{ |
|||
incc.CollectionChanged += OnSelectedItemsCollectionChanged; |
|||
} |
|||
} |
|||
|
|||
private void UnsubscribeFromSelectedItems() |
|||
{ |
|||
if (_selectedItems is INotifyCollectionChanged incc) |
|||
{ |
|||
incc.CollectionChanged += OnSelectedItemsCollectionChanged; |
|||
} |
|||
} |
|||
|
|||
private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) |
|||
{ |
|||
if (_ignoreModelChanges) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var items = SelectedItems; |
|||
var deselected = e.DeselectedItems.ToList(); |
|||
var selected = e.SelectedItems.ToList(); |
|||
|
|||
_ignoreSelectedItemsChanges = true; |
|||
|
|||
foreach (var i in deselected) |
|||
{ |
|||
items.Remove(i); |
|||
} |
|||
|
|||
foreach (var i in selected) |
|||
{ |
|||
items.Add(i); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_ignoreSelectedItemsChanges = false; |
|||
} |
|||
} |
|||
|
|||
private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems(); |
|||
|
|||
private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (_ignoreSelectedItemsChanges) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_selectedItems == null) |
|||
{ |
|||
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); |
|||
} |
|||
|
|||
void Remove() |
|||
{ |
|||
foreach (var i in e.OldItems) |
|||
{ |
|||
var index = IndexOf(Source, i); |
|||
|
|||
if (index != -1) |
|||
{ |
|||
Deselect(index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
try |
|||
{ |
|||
using var operation = BatchUpdate(); |
|||
|
|||
_ignoreModelChanges = 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: |
|||
Clear(); |
|||
Add(_selectedItems); |
|||
break; |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_ignoreModelChanges = false; |
|||
} |
|||
} |
|||
|
|||
private void Add(IList newItems) |
|||
{ |
|||
foreach (var i in newItems) |
|||
{ |
|||
var index = IndexOf(Source, i); |
|||
|
|||
if (index != -1) |
|||
{ |
|||
Select(index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,283 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Specialized; |
|||
using System.ComponentModel; |
|||
using System.Linq; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls.Selection; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
/// <summary>
|
|||
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
|
|||
/// </summary>
|
|||
internal class SelectedItemsSync : IDisposable |
|||
{ |
|||
private ISelectionModel _selectionModel; |
|||
private IList _selectedItems; |
|||
private bool _updatingItems; |
|||
private bool _updatingModel; |
|||
|
|||
public SelectedItemsSync(ISelectionModel model) |
|||
{ |
|||
_selectionModel = model ?? throw new ArgumentNullException(nameof(model)); |
|||
_selectedItems = new AvaloniaList<object?>(); |
|||
SyncSelectedItemsWithSelectionModel(); |
|||
SubscribeToSelectedItems(_selectedItems); |
|||
SubscribeToSelectionModel(model); |
|||
} |
|||
|
|||
public ISelectionModel SelectionModel |
|||
{ |
|||
get => _selectionModel; |
|||
set |
|||
{ |
|||
if (_selectionModel != value) |
|||
{ |
|||
value = value ?? throw new ArgumentNullException(nameof(value)); |
|||
UnsubscribeFromSelectionModel(_selectionModel); |
|||
_selectionModel = value; |
|||
SubscribeToSelectionModel(_selectionModel); |
|||
SyncSelectedItemsWithSelectionModel(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IList SelectedItems |
|||
{ |
|||
get => _selectedItems; |
|||
set |
|||
{ |
|||
value ??= new AvaloniaList<object?>(); |
|||
|
|||
if (_selectedItems != value) |
|||
{ |
|||
if (value.IsFixedSize) |
|||
{ |
|||
throw new NotSupportedException( |
|||
"Cannot assign fixed size selection to SelectedItems."); |
|||
} |
|||
|
|||
UnsubscribeFromSelectedItems(_selectedItems); |
|||
_selectedItems = value; |
|||
SubscribeToSelectedItems(_selectedItems); |
|||
SyncSelectionModelWithSelectedItems(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
UnsubscribeFromSelectedItems(_selectedItems); |
|||
UnsubscribeFromSelectionModel(_selectionModel); |
|||
} |
|||
|
|||
private void SyncSelectedItemsWithSelectionModel() |
|||
{ |
|||
_updatingItems = true; |
|||
|
|||
try |
|||
{ |
|||
_selectedItems.Clear(); |
|||
|
|||
if (_selectionModel.Source is object) |
|||
{ |
|||
foreach (var i in _selectionModel.SelectedItems) |
|||
{ |
|||
_selectedItems.Add(i); |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_updatingItems = false; |
|||
} |
|||
} |
|||
|
|||
private void SyncSelectionModelWithSelectedItems() |
|||
{ |
|||
_updatingModel = true; |
|||
|
|||
try |
|||
{ |
|||
if (_selectionModel.Source is object) |
|||
{ |
|||
using (_selectionModel.BatchUpdate()) |
|||
{ |
|||
SelectionModel.Clear(); |
|||
Add(_selectedItems); |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_updatingModel = false; |
|||
} |
|||
} |
|||
|
|||
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (_updatingItems) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_selectedItems == null) |
|||
{ |
|||
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); |
|||
} |
|||
|
|||
void Remove() |
|||
{ |
|||
foreach (var i in e.OldItems) |
|||
{ |
|||
var index = IndexOf(SelectionModel.Source, i); |
|||
|
|||
if (index != -1) |
|||
{ |
|||
SelectionModel.Deselect(index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
try |
|||
{ |
|||
using var operation = SelectionModel.BatchUpdate(); |
|||
|
|||
_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: |
|||
SelectionModel.Clear(); |
|||
Add(_selectedItems); |
|||
break; |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_updatingModel = false; |
|||
} |
|||
} |
|||
|
|||
private void Add(IList newItems) |
|||
{ |
|||
foreach (var i in newItems) |
|||
{ |
|||
var index = IndexOf(SelectionModel.Source, i); |
|||
|
|||
if (index != -1) |
|||
{ |
|||
SelectionModel.Select(index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == nameof(ISelectionModel.Source)) |
|||
{ |
|||
if (_selectedItems.Count > 0) |
|||
{ |
|||
SyncSelectionModelWithSelectedItems(); |
|||
} |
|||
else |
|||
{ |
|||
SyncSelectedItemsWithSelectionModel(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) |
|||
{ |
|||
if (_updatingModel || _selectionModel.Source is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var deselected = e.DeselectedItems.ToList(); |
|||
var selected = e.SelectedItems.ToList(); |
|||
|
|||
_updatingItems = true; |
|||
|
|||
foreach (var i in deselected) |
|||
{ |
|||
_selectedItems.Remove(i); |
|||
} |
|||
|
|||
foreach (var i in selected) |
|||
{ |
|||
_selectedItems.Add(i); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_updatingItems = false; |
|||
} |
|||
} |
|||
|
|||
private void SelectionModelSourceReset(object sender, EventArgs e) |
|||
{ |
|||
SyncSelectionModelWithSelectedItems(); |
|||
} |
|||
|
|||
|
|||
private void SubscribeToSelectedItems(IList selectedItems) |
|||
{ |
|||
if (selectedItems is INotifyCollectionChanged incc) |
|||
{ |
|||
incc.CollectionChanged += SelectedItemsCollectionChanged; |
|||
} |
|||
} |
|||
|
|||
private void SubscribeToSelectionModel(ISelectionModel model) |
|||
{ |
|||
model.PropertyChanged += SelectionModelPropertyChanged; |
|||
model.SelectionChanged += SelectionModelSelectionChanged; |
|||
model.SourceReset += SelectionModelSourceReset; |
|||
} |
|||
|
|||
private void UnsubscribeFromSelectedItems(IList selectedItems) |
|||
{ |
|||
if (selectedItems is INotifyCollectionChanged incc) |
|||
{ |
|||
incc.CollectionChanged -= SelectedItemsCollectionChanged; |
|||
} |
|||
} |
|||
|
|||
private void UnsubscribeFromSelectionModel(ISelectionModel model) |
|||
{ |
|||
model.PropertyChanged -= SelectionModelPropertyChanged; |
|||
model.SelectionChanged -= SelectionModelSelectionChanged; |
|||
model.SourceReset -= SelectionModelSourceReset; |
|||
} |
|||
|
|||
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,243 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls.Selection; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests.Selection |
|||
{ |
|||
public class InternalSelectionModelTests |
|||
{ |
|||
[Fact] |
|||
public void Selecting_Item_Adds_To_SelectedItems() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.Select(0); |
|||
|
|||
Assert.Equal(new[] { "foo" }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Selecting_Duplicate_On_Model_Adds_To_SelectedItems() |
|||
{ |
|||
var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); |
|||
|
|||
target.SelectRange(1, 4); |
|||
|
|||
Assert.Equal(new[] { "bar", "baz", "foo", "bar" }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Deselecting_On_Model_Removes_SelectedItem() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.SelectRange(1, 2); |
|||
target.Deselect(1); |
|||
|
|||
Assert.Equal(new[] { "baz" }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Deselecting_Duplicate_On_Model_Removes_SelectedItem() |
|||
{ |
|||
var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); |
|||
|
|||
target.SelectRange(1, 2); |
|||
target.Select(4); |
|||
target.Deselect(4); |
|||
|
|||
Assert.Equal(new[] { "baz", "bar" }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Adding_To_SelectedItems_Selects_On_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.SelectRange(1, 2); |
|||
target.SelectedItems.Add("foo"); |
|||
|
|||
Assert.Equal(new[] { 0, 1, 2 }, target.SelectedIndexes); |
|||
Assert.Equal(new[] { "bar", "baz", "foo" }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_From_SelectedItems_Deselects_On_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.SelectRange(1, 2); |
|||
target.SelectedItems.Remove("baz"); |
|||
|
|||
Assert.Equal(new[] { 1 }, target.SelectedIndexes); |
|||
Assert.Equal(new[] { "bar" }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Replacing_SelectedItem_Updates_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.SelectRange(1, 2); |
|||
target.SelectedItems[0] = "foo"; |
|||
|
|||
Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes); |
|||
Assert.Equal(new[] { "foo", "baz" }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Clearing_SelectedItems_Updates_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.SelectedItems.Clear(); |
|||
|
|||
Assert.Empty(target.SelectedIndexes); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_SelectedItems_Updates_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var oldItems = target.SelectedItems; |
|||
|
|||
var newItems = new AvaloniaList<string> { "foo", "baz" }; |
|||
target.SelectedItems = newItems; |
|||
|
|||
Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes); |
|||
Assert.Same(newItems, target.SelectedItems); |
|||
Assert.NotSame(oldItems, target.SelectedItems); |
|||
Assert.Equal(new[] { "foo", "baz" }, newItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_Items_To_Null_Clears_Selection() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.SelectRange(1, 2); |
|||
target.SelectedItems = null; |
|||
|
|||
Assert.Empty(target.SelectedIndexes); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_Items_To_Null_Creates_Empty_Items() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var oldItems = target.SelectedItems; |
|||
|
|||
target.SelectedItems = null; |
|||
|
|||
Assert.NotNull(target.SelectedItems); |
|||
Assert.NotSame(oldItems, target.SelectedItems); |
|||
Assert.IsType<AvaloniaList<object>>(target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Adds_Null_SelectedItems_When_Source_Is_Null() |
|||
{ |
|||
var target = CreateTarget(nullSource: true); |
|||
|
|||
target.SelectRange(1, 2); |
|||
Assert.Equal(new object[] { null, null }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Updates_SelectedItems_When_Source_Changes_From_Null() |
|||
{ |
|||
var target = CreateTarget(nullSource: true); |
|||
|
|||
target.SelectRange(1, 2); |
|||
Assert.Equal(new object[] { null, null }, target.SelectedItems); |
|||
|
|||
target.Source = new[] { "foo", "bar", "baz" }; |
|||
Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Updates_SelectedItems_When_Source_Changes_To_Null() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.SelectRange(1, 2); |
|||
Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); |
|||
|
|||
target.Source = null; |
|||
Assert.Equal(new object[] { null, null }, target.SelectedItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SelectedItems_Can_Be_Set_Before_Source() |
|||
{ |
|||
var target = CreateTarget(nullSource: true); |
|||
var items = new AvaloniaList<string> { "foo", "bar", "baz" }; |
|||
var selectedItems = new AvaloniaList<string> { "bar" }; |
|||
|
|||
target.SelectedItems = selectedItems; |
|||
target.Source = items; |
|||
|
|||
Assert.Equal(1, target.SelectedIndex); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Does_Not_Accept_Fixed_Size_Items() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
Assert.Throws<NotSupportedException>(() => |
|||
target.SelectedItems = new[] { "foo", "bar", "baz" }); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Restores_Selection_On_Items_Reset() |
|||
{ |
|||
var items = new ResettingCollection(new[] { "foo", "bar", "baz" }); |
|||
var target = CreateTarget(source: items); |
|||
|
|||
target.SelectedIndex = 1; |
|||
items.Reset(new[] { "baz", "foo", "bar" }); |
|||
|
|||
Assert.Equal(2, target.SelectedIndex); |
|||
} |
|||
|
|||
private static InternalSelectionModel CreateTarget( |
|||
bool singleSelect = false, |
|||
IList source = null, |
|||
bool nullSource = false) |
|||
{ |
|||
source ??= !nullSource ? new[] { "foo", "bar", "baz" } : null; |
|||
|
|||
var result = new InternalSelectionModel |
|||
{ |
|||
SingleSelect = singleSelect, |
|||
}; |
|||
|
|||
((ISelectionModel)result).Source = source; |
|||
return result; |
|||
} |
|||
|
|||
private class ResettingCollection : List<string>, INotifyCollectionChanged |
|||
{ |
|||
public ResettingCollection(IEnumerable<string> items) |
|||
{ |
|||
AddRange(items); |
|||
} |
|||
|
|||
public void Reset(IEnumerable<string> items) |
|||
{ |
|||
Clear(); |
|||
AddRange(items); |
|||
CollectionChanged?.Invoke( |
|||
this, |
|||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); |
|||
} |
|||
|
|||
public event NotifyCollectionChangedEventHandler CollectionChanged; |
|||
} |
|||
} |
|||
} |
|||
@ -1,278 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls.Selection; |
|||
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.SelectedItems; |
|||
|
|||
Assert.Equal(new[] { "bar", "baz" }, items); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Selecting_On_Model_Adds_Item() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = target.SelectedItems; |
|||
|
|||
target.SelectionModel.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.SelectedItems; |
|||
|
|||
target.SelectionModel.Select(4); |
|||
|
|||
Assert.Equal(new[] { "bar", "baz", "bar" }, items); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Deselecting_On_Model_Removes_Item() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = target.SelectedItems; |
|||
|
|||
target.SelectionModel.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.SelectedItems; |
|||
|
|||
target.SelectionModel.Select(4); |
|||
target.SelectionModel.Deselect(4); |
|||
|
|||
Assert.Equal(new[] { "baz", "bar" }, items); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Reassigning_Model_Resets_Items() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = target.SelectedItems; |
|||
|
|||
var newModel = new SelectionModel<string> |
|||
{ |
|||
Source = (string[])target.SelectionModel.Source, |
|||
SingleSelect = false |
|||
}; |
|||
|
|||
newModel.Select(0); |
|||
newModel.Select(1); |
|||
|
|||
target.SelectionModel = newModel; |
|||
|
|||
Assert.Equal(new[] { "foo", "bar" }, items); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Reassigning_Model_Tracks_New_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = target.SelectedItems; |
|||
|
|||
var newModel = new SelectionModel<string> |
|||
{ |
|||
Source = (string[])target.SelectionModel.Source, |
|||
SingleSelect = false |
|||
}; |
|||
|
|||
target.SelectionModel = 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.SelectedItems; |
|||
|
|||
items.Add("foo"); |
|||
|
|||
Assert.Equal(new[] { 0, 1, 2 }, target.SelectionModel.SelectedIndexes); |
|||
Assert.Equal(new[] { "bar", "baz", "foo" }, items); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_From_Items_Deselects_On_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = target.SelectedItems; |
|||
|
|||
items.Remove("baz"); |
|||
|
|||
Assert.Equal(new[] { 1 }, target.SelectionModel.SelectedIndexes); |
|||
Assert.Equal(new[] { "bar" }, items); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Replacing_Item_Updates_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = target.SelectedItems; |
|||
|
|||
items[0] = "foo"; |
|||
|
|||
Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes); |
|||
Assert.Equal(new[] { "foo", "baz" }, items); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Clearing_Items_Updates_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = target.SelectedItems; |
|||
|
|||
items.Clear(); |
|||
|
|||
Assert.Empty(target.SelectionModel.SelectedIndexes); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_Items_Updates_Model() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var oldItems = target.SelectedItems; |
|||
|
|||
var newItems = new AvaloniaList<string> { "foo", "baz" }; |
|||
target.SelectedItems = newItems; |
|||
|
|||
Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes); |
|||
Assert.Same(newItems, target.SelectedItems); |
|||
Assert.NotSame(oldItems, target.SelectedItems); |
|||
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.SelectedItems = items; |
|||
target.SelectionModel.Select(1); |
|||
|
|||
Assert.Equal(new[] { "foo", "baz", "bar" }, items); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_Items_To_Null_Creates_Empty_Items() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var oldItems = target.SelectedItems; |
|||
|
|||
target.SelectedItems = null; |
|||
|
|||
var newItems = Assert.IsType<AvaloniaList<object>>(target.SelectedItems); |
|||
|
|||
Assert.NotSame(oldItems, newItems); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Handles_Null_Model_Source() |
|||
{ |
|||
var model = new SelectionModel<string> { SingleSelect = false }; |
|||
model.Select(1); |
|||
|
|||
var target = new SelectedItemsSync(model); |
|||
var items = target.SelectedItems; |
|||
|
|||
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.SelectedItems = new[] { "foo", "bar", "baz" }); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source() |
|||
{ |
|||
var model = new SelectionModel<string>(); |
|||
var target = new SelectedItemsSync(model); |
|||
var items = new AvaloniaList<string> { "foo", "bar", "baz" }; |
|||
var selectedItems = new AvaloniaList<string> { "bar" }; |
|||
|
|||
target.SelectedItems = selectedItems; |
|||
model.Source = items; |
|||
|
|||
Assert.Equal(1, model.SelectedIndex); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Restores_Selection_On_Items_Reset() |
|||
{ |
|||
var items = new ResettingCollection(new[] { "foo", "bar", "baz" }); |
|||
var model = new SelectionModel<string> { Source = items }; |
|||
var target = new SelectedItemsSync(model); |
|||
|
|||
model.SelectedIndex = 1; |
|||
items.Reset(new[] { "baz", "foo", "bar" }); |
|||
|
|||
Assert.Equal(2, model.SelectedIndex); |
|||
} |
|||
|
|||
private static SelectedItemsSync CreateTarget( |
|||
IEnumerable<string> items = null) |
|||
{ |
|||
items ??= new[] { "foo", "bar", "baz" }; |
|||
|
|||
var model = new SelectionModel<string> { Source = items, SingleSelect = false }; |
|||
model.SelectRange(1, 2); |
|||
|
|||
var target = new SelectedItemsSync(model); |
|||
return target; |
|||
} |
|||
|
|||
private class ResettingCollection : List<string>, INotifyCollectionChanged |
|||
{ |
|||
public ResettingCollection(IEnumerable<string> items) |
|||
{ |
|||
AddRange(items); |
|||
} |
|||
|
|||
public void Reset(IEnumerable<string> items) |
|||
{ |
|||
Clear(); |
|||
AddRange(items); |
|||
CollectionChanged?.Invoke( |
|||
this, |
|||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); |
|||
} |
|||
|
|||
public event NotifyCollectionChangedEventHandler CollectionChanged; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue