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? items = null, - bool useItemTemplate = true) + bool useItemTemplate = true, + IEnumerable