Browse Source

Fixes #17157 (#17171)

pull/17516/head
mpylon 1 year ago
committed by GitHub
parent
commit
a5c0f2bde2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs
  2. 2
      src/Avalonia.Base/Collections/AvaloniaList.cs
  3. 21
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  4. 22
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  5. 27
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  6. 22
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  7. 14
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  8. 101
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  9. 15
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  10. 24
      tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs
  11. 62
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

5
src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs

@ -70,11 +70,6 @@ namespace Avalonia.Collections
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
Remove(e.OldItems!);
int newIndex = e.NewStartingIndex;
if(newIndex > e.OldStartingIndex)
{
newIndex -= e.OldItems!.Count;
}
Add(e.NewItems!);
break;

2
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -466,7 +466,7 @@ namespace Avalonia.Collections
if (newIndex > oldIndex)
{
modifiedNewIndex -= count;
modifiedNewIndex -= count - 1;
}
_inner.InsertRange(modifiedNewIndex, items);

21
src/Avalonia.Base/Collections/AvaloniaListExtensions.cs

@ -99,15 +99,30 @@ namespace Avalonia.Collections
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!);
int newIndex = e.NewStartingIndex;
var newIndex = e.NewStartingIndex;
if(newIndex > e.OldStartingIndex)
{
newIndex -= e.OldItems!.Count;
newIndex -= e.OldItems!.Count - 1;
}
Add(newIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Replace:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
Remove(e.OldStartingIndex, e.OldItems!);

22
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -93,10 +93,30 @@ namespace Avalonia.Controls.Presenters
Remove(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!.Count);
var insertIndex = e.NewStartingIndex;
if (e.NewStartingIndex > e.OldStartingIndex)
{
insertIndex -= e.OldItems.Count - 1;
}
Add(insertIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
ClearItemsControlLogicalChildren();
children.Clear();

27
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -139,13 +139,38 @@ namespace Avalonia.Controls.Selection
break;
}
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
{
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = removeChange.ShiftIndex;
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
removed = removeChange.RemovedItems;
break;
}
case NotifyCollectionChangedAction.Move:
{
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
var insertIndex = e.NewStartingIndex;
if (e.NewStartingIndex > e.OldStartingIndex)
{
insertIndex -= e.OldItems!.Count - 1;
}
var addChange = OnItemsAdded(insertIndex, e.NewItems!);
shiftIndex = removeChange.ShiftIndex;
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
removed = removeChange.RemovedItems;
}
break;
case NotifyCollectionChangedAction.Reset:

22
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -236,10 +236,30 @@ namespace Avalonia.Controls
Remove(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!.Count);
Add(e.NewStartingIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!.Count);
var insertIndex = e.NewStartingIndex;
if (e.NewStartingIndex > e.OldStartingIndex)
{
insertIndex -= e.OldItems.Count - 1;
}
Add(insertIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Reset:
if (_realized is not null)
{

14
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -272,8 +272,20 @@ namespace Avalonia.Controls
_realizedElements.ItemsReplaced(e.OldStartingIndex, e.OldItems!.Count, _recycleElementOnItemRemoved);
break;
case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
_realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved);
_realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex);
var insertIndex = e.NewStartingIndex;
if (e.NewStartingIndex > e.OldStartingIndex)
{
insertIndex -= e.OldItems.Count - 1;
}
_realizedElements.ItemsInserted(insertIndex, e.NewItems!.Count, _updateElementIndex);
break;
case NotifyCollectionChangedAction.Reset:
_realizedElements.ItemsReset(_recycleElementOnItemRemoved);

101
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@ -52,6 +52,22 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Throws<ArgumentOutOfRangeException>(() => target.InsertRange(1, new List<int>() { 1 }));
}
[Fact]
public void Move_Should_Move_One_Item()
{
AvaloniaList<int> target = [1, 2, 3];
AssertEvent(target, () => target.Move(0, 1), [
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Move,
new[] { 1 },
1,
0)
]);
Assert.Equal(new[] { 2, 1, 3 }, target);
}
[Fact]
public void Move_Should_Update_Collection()
{
@ -75,9 +91,9 @@ namespace Avalonia.Base.UnitTests.Collections
[Fact]
public void MoveRange_Can_Move_To_End()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
AvaloniaList<int> target = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
target.MoveRange(0, 5, 10);
target.MoveRange(0, 5, 9);
Assert.Equal(new[] { 6, 7, 8, 9, 10, 1, 2, 3, 4, 5 }, target);
}
@ -85,25 +101,35 @@ namespace Avalonia.Base.UnitTests.Collections
[Fact]
public void MoveRange_Raises_Correct_CollectionChanged_Event()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
var raised = false;
AvaloniaList<int> target = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Move, e.Action);
Assert.Equal(0, e.OldStartingIndex);
Assert.Equal(10, e.NewStartingIndex);
Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, e.OldItems);
Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, e.NewItems);
raised = true;
};
AssertEvent(target, () => target.MoveRange(0, 9, 9), [
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Move,
new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 },
9,
0)
]);
target.MoveRange(0, 9, 10);
Assert.True(raised);
Assert.Equal(new[] { 10, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, target);
}
[Fact]
public void MoveRange_Should_Move_One_Item()
{
AvaloniaList<int> target = [1, 2, 3];
AssertEvent(target, () => target.MoveRange(0, 1, 1), [
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Move,
new[] { 1 },
1,
0)
]);
Assert.Equal(new[] { 2, 1, 3 }, target);
}
[Fact]
public void Adding_Item_Should_Raise_CollectionChanged()
{
@ -526,5 +552,48 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Equal(0, raised);
}
/// <summary>
/// Assert that <see cref="items"/> emits <see cref="expectedEvents"/> when performing <see cref="action"/>.
/// </summary>
/// <param name="items">The event source.</param>
/// <param name="action">The action to perform.</param>
/// <param name="expectedEvents">The expected events.</param>
private static void AssertEvent(INotifyCollectionChanged items, Action action, NotifyCollectionChangedEventArgs[] expectedEvents)
{
var callCount = 0;
items.CollectionChanged += OnCollectionChanged;
Assert.Multiple(() =>
{
try
{
action();
}
finally
{
items.CollectionChanged -= OnCollectionChanged;
}
Assert.Equal(expectedEvents.Length, callCount);
});
return;
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs actualEvent)
{
Assert.Multiple(() =>
{
Assert.True(callCount < expectedEvents.Length);
Assert.Equal(expectedEvents[callCount].Action, actualEvent.Action);
Assert.Equal(expectedEvents[callCount].NewItems, actualEvent.NewItems);
Assert.Equal(expectedEvents[callCount].NewStartingIndex, actualEvent.NewStartingIndex);
Assert.Equal(expectedEvents[callCount].OldItems, actualEvent.OldItems);
Assert.Equal(expectedEvents[callCount].OldStartingIndex, actualEvent.OldStartingIndex);
});
++callCount;
}
}
}
}

15
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.UnitTests;
@ -91,6 +92,20 @@ namespace Avalonia.Controls.UnitTests.Presenters
AssertContainers(panel, items);
}
[Fact]
public void Updates_Container_For_Moved_Range_Of_Items()
{
using var app = Start();
AvaloniaList<string> items = ["foo", "bar", "baz"];
var (target, _, root) = CreateTarget(items);
var panel = Assert.IsType<StackPanel>(target.Panel);
items.MoveRange(0, 2, 2);
root.LayoutManager.ExecuteLayoutPass();
AssertContainers(panel, items);
}
[Fact]
public void Updates_Containers_For_Replaced_Items()
{

24
tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs

@ -1,9 +1,11 @@
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -110,6 +112,28 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("bar", container.Content);
}
[Fact]
public void Handles_Moved_Item_Range()
{
using var app = Start();
AvaloniaList<string> items = ["foo", "bar", "baz", "qux", "quux"];
var (target, carousel) = CreateTarget(items);
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
carousel.SelectedIndex = 3;
Layout(target);
items.MoveRange(0, 2, 4);
Layout(target);
Assert.Multiple(() =>
{
Assert.Single(target.Children);
Assert.Same(container, target.Children[0]);
Assert.Equal("qux", container.Content);
Assert.Equal(1, carousel.SelectedIndex);
});
}
public class Transitions
{
[Fact]

62
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -204,6 +204,68 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(elements, target.GetRealizedElements());
}
[Fact]
public void Updates_Elements_On_Item_Moved()
{
// Arrange
using var app = App();
var actualItems = new AvaloniaList<string>(Enumerable
.Range(0, 100)
.Select(x => $"Item {x}"));
var (target, _, itemsControl) = CreateTarget(items: actualItems);
var expectedRealizedElementContents = new[] { 1, 2, 0, 3, 4, 5, 6, 7, 8, 9 }
.Select(x => $"Item {x}");
// Act
actualItems.Move(0, 2);
Layout(target);
// Assert
var actualRealizedElementContents = target
.GetRealizedElements()
.Cast<ContentPresenter>()
.Select(x => x.Content);
Assert.Equivalent(expectedRealizedElementContents, actualRealizedElementContents);
}
[Fact]
public void Updates_Elements_On_Item_Range_Moved()
{
// Arrange
using var app = App();
var actualItems = new AvaloniaList<string>(Enumerable
.Range(0, 100)
.Select(x => $"Item {x}"));
var (target, _, itemsControl) = CreateTarget(items: actualItems);
var expectedRealizedElementContents = new[] { 2, 0, 1, 3, 4, 5, 6, 7, 8, 9 }
.Select(x => $"Item {x}");
// Act
actualItems.MoveRange(0, 2, 3);
Layout(target);
// Assert
var actualRealizedElementContents = target
.GetRealizedElements()
.Cast<ContentPresenter>()
.Select(x => x.Content);
Assert.Equivalent(expectedRealizedElementContents, actualRealizedElementContents);
}
[Fact]
public void Updates_Elements_On_Item_Remove()
{

Loading…
Cancel
Save