Repository: AvaloniaUI/Avalonia

Issue:
`INotifyCollectionChanged` is implemented inconsistently for `Move` and `Replace` operations.
### Describe the bug

Various `INotifyCollectionChanged.CollectionChanged` event handlers in Avalonia have an off-by-one error and / or fail to correctly respond to multi-item change events. For example, the following event handler does not respond to multi-item `Move` or `Replace` operations correctly.

https://github.com/AvaloniaUI/Avalonia/blob/981f47f05035b700a3b723653d887f338dfbcc1b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs#L95-L99

To correctly implement this type of behavior, we should follow the example of MAUI.

https://github.com/dotnet/maui/blob/cc683f0e99547c2ed4875296d7451332ef64ca73/src/Controls/src/Core/Internals/NotifyCollectionChangedEventArgsExtensions.cs#L47-L61

Furthermore, the event publishers are not consistent in their interpretations of `INotifyCollectionChanged`. For example, the `AvaloniaList.Move` and `AvaloniaList.MoveRange` methods produce different events for the same mutation (or, equivalently, the same events for different mutations). See the below unit test reflecting this incongruity.

```csharp
[TestFixture]
public class Tests
{
    [Test]
    public void Move()
    {
        AvaloniaList<int> items = [1, 2, 3];

        using (new AssertionScope())
        {
            AssertEvent(items, () => items.Move(0, 1), new NotifyCollectionChangedEventArgs(
                action: NotifyCollectionChangedAction.Move,
                changedItems: new[] { 1 },
                index: 1,
                oldIndex: 0));

            items.ToArray().Should().BeEquivalentTo(new[] { 2, 1, 3 }, o => o.WithStrictOrdering());
        }
    }

    [Test]
    public void MoveRange()
    {
        AvaloniaList<int> items = [1, 2, 3];

        using (new AssertionScope())
        {
            AssertEvent(items, () => items.MoveRange(0, 1, 1), new NotifyCollectionChangedEventArgs(
                action: NotifyCollectionChangedAction.Move,
                changedItems: new[] { 1 },
                index: 1,
                oldIndex: 0));

            items.ToArray().Should().BeEquivalentTo(new[] { 2, 1, 3 }, o => o.WithStrictOrdering());
        }
    }

    private static void AssertEvent(INotifyCollectionChanged items, Action action, NotifyCollectionChangedEventArgs expectedEvent)
    {
        items.CollectionChanged += OnCollectionChanged;

        try
        {
            action();
        }
        finally
        {
            items.CollectionChanged -= OnCollectionChanged;
        }

        return;

        void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs actualEvent)
        {
            actualEvent.Should().BeEquivalentTo(expectedEvent);
        }
    }
}
```

### To Reproduce

To reproduce one of these behaviors, call `AvaloniaList.MoveRange` and verify that it successfully mutates the list, but fails to correctly update the `PanelContainerGenerator`.

```csharp
[AvaloniaTest]
public void MoveRange()
{
    // Arrange

    AvaloniaList<int> items = [1];

    var itemsControl = new ItemsControl
    {
        ItemsPanel = new FuncTemplate<Panel?>(() => new Panel()),
        ItemsSource = items
    };

    var window = new Window
    {
        Content = itemsControl,
        Height = 768,
        Width = 1024
    };

    window.Show();

    // Act

    items.MoveRange(0, 1, 1);

    // Assert

    var actualContents = itemsControl
        .ItemsPanelRoot!
        .GetVisualChildren()
        .Cast<ContentPresenter>()
        .Select(x => x.Content);

    actualContents
        .Should()
        .BeEquivalentTo(items, o => o.WithStrictOrdering());
}
```

### Expected behavior

- The `AvaloniaList.Move` and `AvaloniaList.MoveRange` operations should throw exceptions before emitting events that will cause subsequent listeners to throw exceptions due to mismatched indices.
- `Move(X, Y)` should have the same meaning as `MoveRange(X, 1, Y)`.
- `INotifyCollectionChanged` handlers throughout Avalonia should interpret indices in the same way.

### Avalonia version

11.2.0-beta2

### OS

Windows

### Additional context

_No response_


Hints:

Patch:
diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs
index 8c731c188f4..dfd62259e43 100644
--- a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs
@@ -70,11 +70,6 @@ void Remove(IEnumerable items)
                     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;
 
diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs
index 84a77810da1..324e33e5aff 100644
--- a/src/Avalonia.Base/Collections/AvaloniaList.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaList.cs
@@ -466,7 +466,7 @@ public void MoveRange(int oldIndex, int count, int newIndex)
 
             if (newIndex > oldIndex)
             {
-                modifiedNewIndex -= count;
+                modifiedNewIndex -= count - 1;
             }
 
             _inner.InsertRange(modifiedNewIndex, items);
diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
index 2178577eb71..244788c63e9 100644
--- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
@@ -99,15 +99,30 @@ void Remove(int index, IList items)
                         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!);
diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
index 4027cd252f0..b0e2af2f3ad 100644
--- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
+++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
@@ -93,10 +93,30 @@ void Remove(int index, int count)
                     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();
diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs
index caeff61f074..c50f77830f1 100644
--- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs
+++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs
@@ -139,13 +139,38 @@ protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArg
                         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:
diff --git a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
index d10ae124c12..8b67f01fb15 100644
--- a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
@@ -236,10 +236,30 @@ void Remove(int index, int count)
                     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)
                     {
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index 7934e0ccc6b..3e08b59c2e3 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -272,8 +272,20 @@ protected override void OnItemsChanged(IReadOnlyList<object?> items, NotifyColle
                     _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);


Test patch:
diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
index b59f272b0cb..82a9885ddce 100644
--- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
@@ -52,6 +52,22 @@ public void InsertRange_Past_End_Should_Throw_Exception()
             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 @@ public void MoveRange_Should_Update_Collection()
         [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 @@ public void MoveRange_Can_Move_To_End()
         [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 @@ public void RemoveAll_Should_Handle_Empty_List()
 
             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;
+            }
+        }
     }
 }
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
index ca98991c4b2..24aff3b5664 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
@@ -2,6 +2,7 @@
 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 @@ public void Updates_Containers_For_Moved_Items()
                 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()
             {
diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs
index 4f3b2f588f7..b636c69488c 100644
--- a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs
+++ b/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 @@ public void Handles_Moved_Item()
             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]
diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
index d1ee1662f05..e0fdb6e4858 100644
--- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
@@ -197,6 +197,68 @@ public void Creates_Elements_On_Item_Insert_2()
             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()
         {