From 791ea8beef1cd3aad574ff5d1f42299d57f17f34 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Mar 2023 16:13:40 +0100 Subject: [PATCH] Added container lifecycle events. Seems we weren't calling `ClearItemContainer` for `ItemIsOwnContainer` items in `PanelContainerGenerator`. Fixed that too. --- .../ContainerClearingEventArgs.cs | 20 +++ .../ContainerIndexChangedEventArgs.cs | 32 +++++ .../ContainerPreparedEventArgs.cs | 26 ++++ src/Avalonia.Controls/ItemsControl.cs | 31 +++++ .../Presenters/PanelContainerGenerator.cs | 3 +- .../ItemsControlTests.cs | 130 ++++++++++++++++++ 6 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/ContainerClearingEventArgs.cs create mode 100644 src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs create mode 100644 src/Avalonia.Controls/ContainerPreparedEventArgs.cs 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/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 10a7b0e540..afbc66ef40 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_Called_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_Called_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_Called_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_Called_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_Called_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)