Browse Source

Merge pull request #10055 from AvaloniaUI/fixes/9997-nth-last-child-itemscontrol

Fix nth-last-child styles on virtualizing layouts
pull/10462/head
Max Katz 3 years ago
committed by GitHub
parent
commit
bd5865fca9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  2. 69
      src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
  3. 2
      src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
  4. 47
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  5. 5
      src/Avalonia.Base/Styling/NthChildSelector.cs
  6. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
  7. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
  8. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
  9. 9
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
  10. 15
      src/Avalonia.Controls/ItemsControl.cs
  11. 24
      src/Avalonia.Controls/Panel.cs
  12. 2
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  13. 61
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

2
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -10,7 +10,7 @@
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ListBox ListBoxItem:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Background" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</DockPanel.Styles>

69
src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs

@ -1,28 +1,79 @@
#nullable enable
using System;
using System;
#nullable enable
namespace Avalonia.LogicalTree
{
/// <summary>
/// Describes the action that caused a <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public enum ChildIndexChangedAction
{
/// <summary>
/// The index of a single child changed.
/// </summary>
ChildIndexChanged,
/// <summary>
/// The index of multiple children changed and all children should be re-evaluated.
/// </summary>
ChildIndexesReset,
/// <summary>
/// The total number of children changed.
/// </summary>
TotalCountChanged,
}
/// <summary>
/// Event args for <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public class ChildIndexChangedEventArgs : EventArgs
{
public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs();
private ChildIndexChangedEventArgs()
/// <summary>
/// Initializes a new instance of the <see cref="ChildIndexChangedEventArgs"/> class with
/// an action of <see cref="ChildIndexChangedAction.ChildIndexChanged"/>.
/// </summary>
/// <param name="child">The child whose index was changed.</param>
/// <param name="index">The new index of the child.</param>
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;
}
/// <summary>
/// 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.
/// </summary>
public ChildIndexChangedAction Action { get; }
/// <summary>
/// Gets the logical child whose index was changed or null if all children should be re-evaluated.
/// </summary>
public ILogical? Child { get; }
/// <summary>
/// Gets the new index of <see cref="Child"/> or -1 if all children should be re-evaluated.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets an instance of the <see cref="ChildIndexChangedEventArgs"/> with an action of
/// <see cref="ChildIndexChangedAction.ChildIndexesReset"/>.
/// </summary>
public static ChildIndexChangedEventArgs ChildIndexesReset { get; } = new(ChildIndexChangedAction.ChildIndexesReset);
/// <summary>
/// Gets an instance of the <see cref="ChildIndexChangedEventArgs"/> with an action of
/// <see cref="ChildIndexChangedAction.TotalCountChanged"/>.
/// </summary>
public static ChildIndexChangedEventArgs TotalCountChanged { get; } = new(ChildIndexChangedAction.TotalCountChanged);
}
}

2
src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs

@ -25,7 +25,7 @@ namespace Avalonia.LogicalTree
bool TryGetTotalCount(out int count);
/// <summary>
/// Notifies subscriber when child's index or total count was changed.
/// Notifies subscriber when a child's index was changed.
/// </summary>
event EventHandler<ChildIndexChangedEventArgs>? ChildIndexChanged;
}

47
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();
}
}
}

5
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;

2
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)

2
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);
}
}
}

2
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));
}
/// <summary>

9
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)

15
src/Avalonia.Controls/ItemsControl.cs

@ -102,7 +102,6 @@ namespace Avalonia.Controls
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
private IDataTemplate? _displayMemberItemTemplate;
private Tuple<int, Control>? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
private ItemsPresenter? _itemsPresenter;
@ -218,7 +217,6 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value;
}
/// <inheritdoc />
public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
{
@ -495,6 +493,7 @@ namespace Avalonia.Controls
else if (change.Property == ItemCountProperty)
{
UpdatePseudoClasses(change.GetNewValue<int>());
_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;
}

24
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<ChildIndexChangedEventArgs>? 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;
}
}
/// <summary>
@ -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<TPanel>(AvaloniaPropertyChangedEventArgs e)
where TPanel : Panel
{

2
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)

61
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<ContentPresenter>().NthChild(5, 0))
{
Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
};
var (target, _, _) = CreateTarget(styles: new[] { style });
var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().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<ContentPresenter>().NthLastChild(5, 0))
{
Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
};
var (target, _, _) = CreateTarget(styles: new[] { style });
var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().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<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{
return target.GetRealizedElements()
@ -399,7 +453,8 @@ namespace Avalonia.Controls.UnitTests
private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget(
IEnumerable<object>? items = null,
bool useItemTemplate = true)
bool useItemTemplate = true,
IEnumerable<Style>? styles = null)
{
var target = new VirtualizingStackPanel();
@ -428,6 +483,10 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot(true, itemsControl);
root.ClientSize = new(100, 100);
if (styles is not null)
root.Styles.AddRange(styles);
root.LayoutManager.ExecuteInitialLayoutPass();
return (target, scroll, itemsControl);

Loading…
Cancel
Save