diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml
index dc0eaf0a51..b1b1b99c9c 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml
@@ -10,7 +10,7 @@
diff --git a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
index 497596fcc1..6b41c1c66c 100644
--- a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
+++ b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
@@ -1,28 +1,79 @@
-#nullable enable
-using System;
+using System;
+
+#nullable enable
namespace Avalonia.LogicalTree
{
+ ///
+ /// Describes the action that caused a event.
+ ///
+ public enum ChildIndexChangedAction
+ {
+ ///
+ /// The index of a single child changed.
+ ///
+ ChildIndexChanged,
+
+ ///
+ /// The index of multiple children changed and all children should be re-evaluated.
+ ///
+ ChildIndexesReset,
+
+ ///
+ /// The total number of children changed.
+ ///
+ TotalCountChanged,
+ }
+
///
/// Event args for event.
///
public class ChildIndexChangedEventArgs : EventArgs
{
- public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs();
-
- private ChildIndexChangedEventArgs()
+ ///
+ /// Initializes a new instance of the class with
+ /// an action of .
+ ///
+ /// The child whose index was changed.
+ /// The new index of the child.
+ public ChildIndexChangedEventArgs(ILogical child, int index)
{
+ Action = ChildIndexChangedAction.ChildIndexChanged;
+ Child = child;
+ Index = index;
}
- public ChildIndexChangedEventArgs(ILogical child)
+ private ChildIndexChangedEventArgs(ChildIndexChangedAction action)
{
- Child = child;
+ Action = action;
+ Index = -1;
}
///
- /// Logical child which index was changed.
- /// If null, all children should be reset.
+ /// Gets the type of change action that ocurred on the list control.
+ ///
+ public ChildIndexChangedAction Action { get; }
+
+ ///
+ /// Gets the logical child whose index was changed or null if all children should be re-evaluated.
///
public ILogical? Child { get; }
+
+ ///
+ /// Gets the new index of or -1 if all children should be re-evaluated.
+ ///
+ public int Index { get; }
+
+ ///
+ /// Gets an instance of the with an action of
+ /// .
+ ///
+ public static ChildIndexChangedEventArgs ChildIndexesReset { get; } = new(ChildIndexChangedAction.ChildIndexesReset);
+
+ ///
+ /// Gets an instance of the with an action of
+ /// .
+ ///
+ public static ChildIndexChangedEventArgs TotalCountChanged { get; } = new(ChildIndexChangedAction.TotalCountChanged);
}
}
diff --git a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
index 7fcd73273c..186c9527f2 100644
--- a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
+++ b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
@@ -25,7 +25,7 @@ namespace Avalonia.LogicalTree
bool TryGetTotalCount(out int count);
///
- /// Notifies subscriber when child's index or total count was changed.
+ /// Notifies subscriber when a child's index was changed.
///
event EventHandler? ChildIndexChanged;
}
diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
index 33d4cd0824..8bdcec2e53 100644
--- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
+++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
@@ -1,4 +1,5 @@
#nullable enable
+using System;
using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators
@@ -13,6 +14,7 @@ namespace Avalonia.Styling.Activators
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
+ private int _index = -1;
public NthChildActivator(
ILogical control,
@@ -28,24 +30,51 @@ namespace Avalonia.Styling.Activators
protected override bool EvaluateIsActive()
{
- return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
+ var index = _index >= 0 ? _index : _provider.GetChildIndex(_control);
+ return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch;
}
- protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged;
- protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged;
+ protected override void Initialize()
+ {
+ _provider.ChildIndexChanged += ChildIndexChanged;
+ }
+
+ protected override void Deinitialize()
+ {
+ _provider.ChildIndexChanged -= ChildIndexChanged;
+ }
private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
- // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
- // 2. e.Child is null, when all children indices were changed.
- // 3. Subscribed child index was changed.
- if (_reversed
- || e.Child is null
- || e.Child == _control)
+ // 1. Subscribed child index was changed
+ // 2. Child indexes were reset
+ // 3. We're a reversed (nth-last-child) selector and total count has changed
+ if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) ||
+ (_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged))
{
+ // We're using the _index field to pass the index of the child to EvaluateIsActive
+ // *only* when the active state is re-evaluated via this event handler. The docs
+ // for EvaluateIsActive say:
+ //
+ // > This method should read directly from its inputs and not rely on any
+ // > subscriptions to fire in order to be up-to-date.
+ //
+ // Which is good advice in general, however in this case we need to break the rule
+ // and use the value from the event subscription instead of calling
+ // IChildIndexProvider.GetChildIndex. This is because this event can be fired during
+ // the process of realizing an element of a virtualized list; in this case calling
+ // GetChildIndex may not return the correct index as the element isn't yet realized.
+ _index = e.Index;
ReevaluateIsActive();
+ _index = -1;
}
}
+
+ private void TotalCountChanged(object? sender, EventArgs e)
+ {
+ if (_reversed)
+ ReevaluateIsActive();
+ }
}
}
diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs
index ccfc2c781d..532179bb2c 100644
--- a/src/Avalonia.Base/Styling/NthChildSelector.cs
+++ b/src/Avalonia.Base/Styling/NthChildSelector.cs
@@ -61,7 +61,7 @@ namespace Avalonia.Styling
{
return subscribe
? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed))
- : Evaluate(logical, childIndexProvider, Step, Offset, _reversed);
+ : Evaluate(childIndexProvider.GetChildIndex(logical), childIndexProvider, Step, Offset, _reversed);
}
else
{
@@ -70,10 +70,9 @@ namespace Avalonia.Styling
}
internal static SelectorMatch Evaluate(
- ILogical logical, IChildIndexProvider childIndexProvider,
+ int index, IChildIndexProvider childIndexProvider,
int step, int offset, bool reversed)
{
- var index = childIndexProvider.GetChildIndex(logical);
if (index < 0)
{
return SelectorMatch.NeverThisInstance;
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
index 06a77f0894..f5db7c0855 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
@@ -336,7 +336,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex()
{
- _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge)
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
index f9b84793c6..fcf72385b2 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
@@ -423,7 +423,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex()
{
- _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
}
}
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
index d906cd359c..5a4ddd36f4 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
@@ -66,7 +66,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex(DataGridRow row)
{
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row, row.Index));
}
///
diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
index 951e60c25b..3d3d01e06e 100644
--- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
+++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
@@ -536,11 +536,12 @@ namespace Avalonia.Controls
internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo)
{
+ var index = virtInfo.Index;
+
_viewportManager.OnElementPrepared(element, virtInfo);
if (ElementPrepared != null)
{
- var index = virtInfo.Index;
if (_elementPreparedArgs == null)
{
@@ -554,7 +555,7 @@ namespace Avalonia.Controls
ElementPrepared(this, _elementPreparedArgs);
}
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index));
}
internal void OnElementClearing(Control element)
@@ -573,7 +574,7 @@ namespace Avalonia.Controls
ElementClearing(this, _elementClearingArgs);
}
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, -1));
}
internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex)
@@ -592,7 +593,7 @@ namespace Avalonia.Controls
ElementIndexChanged(this, _elementIndexChangedArgs);
}
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, newIndex));
}
private void OnDataSourcePropertyChanged(ItemsSourceView? oldValue, ItemsSourceView? newValue)
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index ce12d5f2bf..9483f98881 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -102,7 +102,6 @@ namespace Avalonia.Controls
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler? _childIndexChanged;
private IDataTemplate? _displayMemberItemTemplate;
- private Tuple? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
private ItemsPresenter? _itemsPresenter;
@@ -218,7 +217,6 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value;
}
-
///
public event EventHandler HorizontalSnapPointsChanged
{
@@ -495,6 +493,7 @@ namespace Avalonia.Controls
else if (change.Property == ItemCountProperty)
{
UpdatePseudoClasses(change.GetNewValue());
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
@@ -579,7 +578,7 @@ namespace Avalonia.Controls
internal void RegisterItemsPresenter(ItemsPresenter presenter)
{
Presenter = presenter;
- _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
internal void PrepareItemContainer(Control container, object? item, int index)
@@ -601,17 +600,14 @@ namespace Avalonia.Controls
internal void ItemContainerPrepared(Control container, object? item, int index)
{
- _containerBeingPrepared = new(index, container);
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
- _containerBeingPrepared = null;
-
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
_scrollViewer?.RegisterAnchorCandidate(container);
}
internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{
ContainerIndexChangedOverride(container, oldIndex, newIndex);
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
}
internal void ClearItemContainer(Control container)
@@ -742,9 +738,6 @@ namespace Avalonia.Controls
int IChildIndexProvider.GetChildIndex(ILogical child)
{
- if (_containerBeingPrepared?.Item2 == child)
- return _containerBeingPrepared.Item1;
-
return child is Control container ? IndexFromContainer(container) : -1;
}
diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs
index a7dc035459..fa18ee468c 100644
--- a/src/Avalonia.Controls/Panel.cs
+++ b/src/Avalonia.Controls/Panel.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.ComponentModel;
using System.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
@@ -60,8 +61,19 @@ namespace Avalonia.Controls
event EventHandler? IChildIndexProvider.ChildIndexChanged
{
- add => _childIndexChanged += value;
- remove => _childIndexChanged -= value;
+ add
+ {
+ if (_childIndexChanged is null)
+ Children.PropertyChanged += ChildrenPropertyChanged;
+ _childIndexChanged += value;
+ }
+
+ remove
+ {
+ _childIndexChanged -= value;
+ if (_childIndexChanged is null)
+ Children.PropertyChanged -= ChildrenPropertyChanged;
+ }
}
///
@@ -152,7 +164,7 @@ namespace Avalonia.Controls
throw new NotSupportedException();
}
- _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
InvalidateMeasureOnChildrenChanged();
}
@@ -161,6 +173,12 @@ namespace Avalonia.Controls
InvalidateMeasure();
}
+ private void ChildrenPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(Children.Count) || e.PropertyName is null)
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
+ }
+
private static void AffectsParentArrangeInvalidate(AvaloniaPropertyChangedEventArgs e)
where TPanel : Panel
{
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index 8e7690aa6c..4970a333a5 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -1167,7 +1167,7 @@ namespace Avalonia.Controls
// Update the indexes of the elements after the removed range.
end = _elements.Count;
- var newIndex = first;
+ var newIndex = first + start;
for (var i = start; i < end; ++i)
{
if (_elements[i] is Control element)
diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
index 55c43f6f96..ba8e7242a1 100644
--- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
@@ -10,6 +10,8 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
+using Avalonia.Media;
+using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
@@ -355,6 +357,58 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Vector(0, 0), scroll.Offset);
}
+ [Fact]
+ public void NthChild_Selector_Works()
+ {
+ using var app = App();
+
+ var style = new Style(x => x.OfType().NthChild(5, 0))
+ {
+ Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
+ };
+
+ var (target, _, _) = CreateTarget(styles: new[] { style });
+ var realized = target.GetRealizedContainers()!.Cast().ToList();
+
+ Assert.Equal(10, realized.Count);
+
+ for (var i = 0; i < 10; ++i)
+ {
+ var container = realized[i];
+ var index = target.IndexFromContainer(container);
+ var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null;
+
+ Assert.Equal(i, index);
+ Assert.Equal(expectedBackground, container.Background);
+ }
+ }
+
+ [Fact]
+ public void NthLastChild_Selector_Works()
+ {
+ using var app = App();
+
+ var style = new Style(x => x.OfType().NthLastChild(5, 0))
+ {
+ Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
+ };
+
+ var (target, _, _) = CreateTarget(styles: new[] { style });
+ var realized = target.GetRealizedContainers()!.Cast().ToList();
+
+ Assert.Equal(10, realized.Count);
+
+ for (var i = 0; i < 10; ++i)
+ {
+ var container = realized[i];
+ var index = target.IndexFromContainer(container);
+ var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null;
+
+ Assert.Equal(i, index);
+ Assert.Equal(expectedBackground, container.Background);
+ }
+ }
+
private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{
return target.GetRealizedElements()
@@ -399,7 +453,8 @@ namespace Avalonia.Controls.UnitTests
private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget(
IEnumerable