diff --git a/src/Avalonia.Controls/ContainerClearingEventArgs.cs b/src/Avalonia.Controls/ContainerClearingEventArgs.cs
new file mode 100644
index 0000000000..fb0c5c4bf8
--- /dev/null
+++ b/src/Avalonia.Controls/ContainerClearingEventArgs.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ContainerClearingEventArgs : EventArgs
+ {
+ public ContainerClearingEventArgs(Control container)
+ {
+ Container = container;
+ }
+
+ ///
+ /// Gets the prepared container.
+ ///
+ public Control Container { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs b/src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs
new file mode 100644
index 0000000000..80536e742c
--- /dev/null
+++ b/src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ContainerIndexChangedEventArgs : EventArgs
+ {
+ public ContainerIndexChangedEventArgs(Control container, int oldIndex, int newIndex)
+ {
+ Container = container;
+ OldIndex = oldIndex;
+ NewIndex = newIndex;
+ }
+
+ ///
+ /// Get the container for which the index changed.
+ ///
+ public Control Container { get; }
+
+ ///
+ /// Gets the index of the container after the change.
+ ///
+ public int NewIndex { get; }
+
+ ///
+ /// Gets the index of the container before the change.
+ ///
+ public int OldIndex { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/ContainerPreparedEventArgs.cs b/src/Avalonia.Controls/ContainerPreparedEventArgs.cs
new file mode 100644
index 0000000000..1acb653bd1
--- /dev/null
+++ b/src/Avalonia.Controls/ContainerPreparedEventArgs.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ContainerPreparedEventArgs : EventArgs
+ {
+ public ContainerPreparedEventArgs(Control container, int index)
+ {
+ Container = container;
+ Index = index;
+ }
+
+ ///
+ /// Gets the prepared container.
+ ///
+ public Control Container { get; }
+
+ ///
+ /// Gets the index of the item the container was prepared for.
+ ///
+ public int Index { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 09ae159047..4460ff0486 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -274,6 +274,34 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value;
}
+ ///
+ /// Occurs each time a container is prepared for use.
+ ///
+ ///
+ /// The prepared element might be newly created or an existing container that is being re-
+ /// used.
+ ///
+ public event EventHandler? ContainerPrepared;
+
+ ///
+ /// Occurs for each realized container when the index for the item it represents has changed.
+ ///
+ ///
+ /// This event is raised for each realized container where the index for the item it
+ /// represents has changed. For example, when another item is added or removed in the data
+ /// source, the index for items that come after in the ordering will be impacted.
+ ///
+ public event EventHandler? ContainerIndexChanged;
+
+ ///
+ /// Occurs each time a container is cleared.
+ ///
+ ///
+ /// This event is raised immediately each time an container is cleared, such as when it
+ /// falls outside the range of realized items or the corresponding item is removed.
+ ///
+ public event EventHandler? ContainerClearing;
+
///
public event EventHandler HorizontalSnapPointsChanged
{
@@ -649,18 +677,21 @@ namespace Avalonia.Controls
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
_scrollViewer?.RegisterAnchorCandidate(container);
+ ContainerPrepared?.Invoke(this, new(container, index));
}
internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{
ContainerIndexChangedOverride(container, oldIndex, newIndex);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
+ ContainerIndexChanged?.Invoke(this, new(container, oldIndex, newIndex));
}
internal void ClearItemContainer(Control container)
{
_scrollViewer?.UnregisterAnchorCandidate(container);
ClearContainerForItemOverride(container);
+ ContainerClearing?.Invoke(this, new(container));
}
private void AddControlItemsToLogicalChildren(IEnumerable? items)
diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
index 1cf0202772..796ee8433a 100644
--- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
+++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
@@ -69,8 +69,7 @@ namespace Avalonia.Controls.Presenters
var c = children[index + i];
if (!c.IsSet(ItemIsOwnContainerProperty))
itemsControl.RemoveLogicalChild(children[i + index]);
- else
- generator.ClearItemContainer(c);
+ generator.ClearItemContainer(c);
}
children.RemoveRange(index, count);
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index 5bfa0966b2..dfacb93eeb 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -1079,7 +1079,7 @@ namespace Avalonia.Controls
// elements after the insertion point.
var elementCount = _elements.Count;
var start = Math.Max(realizedIndex, 0);
- var newIndex = first + count;
+ var newIndex = realizedIndex + count;
for (var i = start; i < elementCount; ++i)
{
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
index 10a7b0e540..41aaa7b670 100644
--- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
@@ -831,6 +832,135 @@ namespace Avalonia.Controls.UnitTests
Assert.Throws(() => target.DisplayMemberBinding = new Binding("Length"));
}
+ [Fact]
+ public void ContainerPrepared_Is_Raised_For_Each_Item_Container_On_Layout()
+ {
+ var target = new ItemsControl
+ {
+ Template = GetTemplate(),
+ Items = { "Foo", "Bar", "Baz" },
+ };
+
+ var result = new List();
+ var index = 0;
+
+ target.ContainerPrepared += (s, e) =>
+ {
+ Assert.Equal(index++, e.Index);
+ result.Add(e.Container);
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ Assert.Equal(3, result.Count);
+ Assert.Equal(target.GetRealizedContainers(), result);
+ }
+
+ [Fact]
+ public void ContainerPrepared_Is_Raised_For_Each_ItemsSource_Container_On_Layout()
+ {
+ var target = new ItemsControl
+ {
+ Template = GetTemplate(),
+ ItemsSource = new[] { "Foo", "Bar", "Baz" },
+ };
+
+ var result = new List();
+ var index = 0;
+
+ target.ContainerPrepared += (s, e) =>
+ {
+ Assert.Equal(index++, e.Index);
+ result.Add(e.Container);
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ Assert.Equal(3, result.Count);
+ Assert.Equal(target.GetRealizedContainers(), result);
+ }
+
+ [Fact]
+ public void ContainerPrepared_Is_Raised_For_Added_Item()
+ {
+ var target = new ItemsControl
+ {
+ Template = GetTemplate(),
+ Items = { "Foo", "Bar", "Baz" },
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ var result = new List();
+
+ target.ContainerPrepared += (s, e) =>
+ {
+ Assert.Equal(3, e.Index);
+ result.Add(e.Container);
+ };
+
+ target.Items.Add("Qux");
+
+ Assert.Equal(1, result.Count);
+ }
+
+ [Fact]
+ public void ContainerIndexChanged_Is_Raised_When_Item_Added()
+ {
+ var target = new ItemsControl
+ {
+ Template = GetTemplate(),
+ Items = { "Foo", "Bar", "Baz" },
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ var result = new List();
+ var index = 1;
+
+ target.ContainerIndexChanged += (s, e) =>
+ {
+ Assert.Equal(index++, e.OldIndex);
+ Assert.Equal(index, e.NewIndex);
+ result.Add(e.Container);
+ };
+
+ target.Items.Insert(1, "Qux");
+
+ Assert.Equal(2, result.Count);
+ Assert.Equal(target.GetRealizedContainers().Skip(2), result);
+ }
+
+ [Fact]
+ public void ContainerClearing_Is_Raised_When_Item_Removed()
+ {
+ var target = new ItemsControl
+ {
+ Template = GetTemplate(),
+ Items = { "Foo", "Bar", "Baz" },
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ var expected = target.ContainerFromIndex(1);
+ var raised = 0;
+
+ target.ContainerClearing += (s, e) =>
+ {
+ Assert.Same(expected, e.Container);
+ ++raised;
+ };
+
+ target.Items.RemoveAt(1);
+
+ Assert.Equal(1, raised);
+ }
+
private class Item
{
public Item(string value)
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
index d2b75a81ca..f42185a59c 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
@@ -785,6 +785,146 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void ContainerPrepared_Is_Raised_For_Each_Item_Container_On_Layout()
+ {
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ Items = { "Foo", "Bar", "Baz" },
+ };
+
+ var result = new List();
+ var index = 0;
+
+ target.ContainerPrepared += (s, e) =>
+ {
+ Assert.Equal(index++, e.Index);
+ result.Add(e.Container);
+ };
+
+ Prepare(target);
+
+ Assert.Equal(3, result.Count);
+ Assert.Equal(target.GetRealizedContainers(), result);
+ }
+
+ [Fact]
+ public void ContainerPrepared_Is_Raised_For_Each_ItemsSource_Container_On_Layout()
+ {
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ ItemsSource = new[] { "Foo", "Bar", "Baz" },
+ };
+
+ var result = new List();
+ var index = 0;
+
+ target.ContainerPrepared += (s, e) =>
+ {
+ Assert.Equal(index++, e.Index);
+ result.Add(e.Container);
+ };
+
+ Prepare(target);
+
+ Assert.Equal(3, result.Count);
+ Assert.Equal(target.GetRealizedContainers(), result);
+ }
+
+ [Fact]
+ public void ContainerPrepared_Is_Raised_For_Added_Item()
+ {
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+ var data = new AvaloniaList { "Foo", "Bar", "Baz" };
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ ItemsSource = data,
+ };
+
+ Prepare(target);
+
+ var result = new List();
+
+ target.ContainerPrepared += (s, e) =>
+ {
+ Assert.Equal(3, e.Index);
+ result.Add(e.Container);
+ };
+
+ data.Add("Qux");
+ Layout(target);
+
+ Assert.Equal(1, result.Count);
+ }
+
+ [Fact]
+ public void ContainerIndexChanged_Is_Raised_When_Item_Added()
+ {
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+ var data = new AvaloniaList { "Foo", "Bar", "Baz" };
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ ItemsSource = data,
+ };
+
+ Prepare(target);
+
+ var result = new List();
+ var index = 1;
+
+ target.ContainerIndexChanged += (s, e) =>
+ {
+ Assert.Equal(index++, e.OldIndex);
+ Assert.Equal(index, e.NewIndex);
+ result.Add(e.Container);
+ };
+
+ data.Insert(1, "Qux");
+ Layout(target);
+
+ Assert.Equal(2, result.Count);
+ Assert.Equal(target.GetRealizedContainers().Skip(2), result);
+ }
+
+ [Fact]
+ public void ContainerClearing_Is_Raised_When_Item_Removed()
+ {
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+ var data = new AvaloniaList { "Foo", "Bar", "Baz" };
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ ItemsSource = data,
+ };
+
+ Prepare(target);
+
+ var expected = target.ContainerFromIndex(1);
+ var raised = 0;
+
+ target.ContainerClearing += (s, e) =>
+ {
+ Assert.Same(expected, e.Container);
+ ++raised;
+ };
+
+ data.RemoveAt(1);
+ Layout(target);
+
+ Assert.Equal(1, raised);
+ }
+
private class ResettingCollection : List, INotifyCollectionChanged
{
public ResettingCollection(int itemCount)
diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
index 2c86ca045e..42b419bbcc 100644
--- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
@@ -406,6 +406,36 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void ContainerPrepared_Is_Raised_When_Scrolling()
+ {
+ using var app = App();
+ var (target, scroll, itemsControl) = CreateTarget();
+ var raised = 0;
+
+ itemsControl.ContainerPrepared += (s, e) => ++raised;
+
+ scroll.Offset = new Vector(0, 200);
+ Layout(target);
+
+ Assert.Equal(10, raised);
+ }
+
+ [Fact]
+ public void ContainerClearing_Is_Raised_When_Scrolling()
+ {
+ using var app = App();
+ var (target, scroll, itemsControl) = CreateTarget();
+ var raised = 0;
+
+ itemsControl.ContainerClearing += (s, e) => ++raised;
+
+ scroll.Offset = new Vector(0, 200);
+ Layout(target);
+
+ Assert.Equal(10, raised);
+ }
+
private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{
return target.GetRealizedElements()