diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
index 93f3c33434..4d0bd663df 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
@@ -12,6 +12,11 @@
+
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml
index 897134badb..cb29f54c94 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml
@@ -3,8 +3,13 @@
x:Class="ControlCatalog.Pages.ListBoxPage">
-
+
diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
index d58ef2e510..b92af1eb9c 100644
--- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
+++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Specialized;
-using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
@@ -133,7 +132,7 @@ namespace Avalonia.Controls.Presenters
protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
- int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : null;
+ int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : (int?)null;
event EventHandler IChildIndexProvider.ChildIndexChanged
{
@@ -161,6 +160,8 @@ namespace Avalonia.Controls.Presenters
if (Panel != null)
{
ItemsChanged(e);
+
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
}
}
@@ -192,7 +193,7 @@ namespace Avalonia.Controls.Presenters
{
for (var i = 0; i < e.Containers.Count; i++)
{
- _childIndexChanged?.Invoke(sender, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl));
}
}
diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs
index 1c90851e13..de41f5292c 100644
--- a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs
+++ b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs
@@ -3,6 +3,9 @@ using System;
namespace Avalonia.LogicalTree
{
+ ///
+ /// Event args for event.
+ ///
public class ChildIndexChangedEventArgs : EventArgs
{
public ChildIndexChangedEventArgs()
diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs
index fdba99baa2..53e2199d28 100644
--- a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs
+++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs
@@ -3,12 +3,30 @@ using System;
namespace Avalonia.LogicalTree
{
+ ///
+ /// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.)
+ ///
+ ///
+ /// Used by nth-child and nth-last-child selectors.
+ ///
public interface IChildIndexProvider
{
+ ///
+ /// Gets child's actual index in order of the original source.
+ ///
+ /// Logical child.
+ /// Index or -1 if child was not found.
int GetChildIndex(ILogical child);
+ ///
+ /// Total children count or null if source is infinite.
+ /// Some Avalonia features might not work if is null, for instance: nth-last-child selector.
+ ///
int? TotalCount { get; }
+ ///
+ /// Notifies subscriber when child's index or total count was changed.
+ ///
event EventHandler? ChildIndexChanged;
}
}
diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs
index 34cca1a396..5d23d1ffd1 100644
--- a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs
+++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs
@@ -44,7 +44,12 @@ namespace Avalonia.Styling.Activators
private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
{
- if (e.Child is null
+ // 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 indeces were changed.
+ // 3. Subscribed child index was changed.
+ if (_reversed
+ || e.Child is null
|| e.Child == _control)
{
PublishNext(IsMatching());
diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs
index 16b97e22f6..e844fb51f8 100644
--- a/src/Avalonia.Styling/Styling/NthChildSelector.cs
+++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs
@@ -7,6 +7,12 @@ using Avalonia.Styling.Activators;
namespace Avalonia.Styling
{
+ ///
+ /// The :nth-child() pseudo-class matches elements based on their position in a group of siblings.
+ ///
+ ///
+ /// Element indices are 1-based.
+ ///
public class NthChildSelector : Selector
{
private const string NthChildSelectorName = "nth-child";
@@ -22,6 +28,12 @@ namespace Avalonia.Styling
_reversed = reversed;
}
+ ///
+ /// Creates an instance of
+ ///
+ /// Previous selector.
+ /// Position step.
+ /// Initial index offset.
public NthChildSelector(Selector? previous, int step, int offset)
: this(previous, step, offset, false)
{
diff --git a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs
index ff7cf0faa1..6f6abbae6a 100644
--- a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs
+++ b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs
@@ -2,8 +2,20 @@
namespace Avalonia.Styling
{
+ ///
+ /// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end.
+ ///
+ ///
+ /// Element indices are 1-based.
+ ///
public class NthLastChildSelector : NthChildSelector
{
+ ///
+ /// Creates an instance of
+ ///
+ /// Previous selector.
+ /// Position step.
+ /// Initial index offset, counting from the end.
public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true)
{
}
diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs
index 0bccccbd7c..64d0a0e96b 100644
--- a/src/Avalonia.Styling/Styling/Selectors.cs
+++ b/src/Avalonia.Styling/Styling/Selectors.cs
@@ -123,11 +123,17 @@ namespace Avalonia.Styling
return new NotSelector(previous, argument);
}
+ ///
+ ///
+ /// The selector.
public static Selector NthChild(this Selector previous, int step, int offset)
{
return new NthChildSelector(previous, step, offset);
}
+ ///
+ ///
+ /// The selector.
public static Selector NthLastChild(this Selector previous, int step, int offset)
{
return new NthLastChildSelector(previous, step, offset);
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
index ee633ee66f..28960c8bf6 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
@@ -336,6 +337,45 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
+ [Fact]
+ public void Style_Can_Use_NthLastChild_Selector_After_Reoder()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+
+ var parent = window.FindControl("parent");
+ var b1 = window.FindControl("b1");
+ var b2 = window.FindControl("b2");
+
+ Assert.Equal(Brushes.Red, b1.Background);
+ Assert.Null(b2.Background);
+
+ parent.Children.Remove(b1);
+
+ Assert.Null(b1.Background);
+ Assert.Null(b2.Background);
+
+ parent.Children.Add(b1);
+
+ Assert.Null(b1.Background);
+ Assert.Equal(Brushes.Red, b2.Background);
+ }
+ }
+
[Fact]
public void Style_Can_Use_NthChild_Selector_With_ListBox()
@@ -364,25 +404,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
window.Show();
- var items = list.Presenter.Panel.Children.Cast();
- ListBoxItem At(int index) => items.ElementAt(index);
+ IEnumerable GetColors() => list.Presenter.Panel.Children.Cast().Select(t => t.Background);
- Assert.Equal(Brushes.Transparent, At(0).Background);
- Assert.Equal(Brushes.Green, At(1).Background);
- Assert.Equal(Brushes.Transparent, At(2).Background);
+ Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
collection.Remove(Brushes.Green);
- Assert.Equal(Brushes.Transparent, At(0).Background);
- Assert.Equal(Brushes.Blue, At(1).Background);
+ Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
collection.Add(Brushes.Violet);
collection.Add(Brushes.Black);
- Assert.Equal(Brushes.Transparent, At(0).Background);
- Assert.Equal(Brushes.Blue, At(1).Background);
- Assert.Equal(Brushes.Transparent, At(2).Background);
- Assert.Equal(Brushes.Black, At(3).Background);
+ Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
}
}
@@ -415,25 +448,19 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
window.Show();
- var items = list.Children;
- TextBlock At(int index) => (TextBlock)list.GetOrCreateElement(index);
+ IEnumerable GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count)
+ .Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground);
- Assert.Equal(Brushes.Transparent, At(0).Foreground);
- Assert.Equal(Brushes.Green, At(1).Foreground);
- Assert.Equal(Brushes.Transparent, At(2).Foreground);
+ Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
collection.Remove(Brushes.Green);
- Assert.Equal(Brushes.Transparent, At(0).Foreground);
- Assert.Equal(Brushes.Blue, At(1).Foreground);
+ Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
collection.Add(Brushes.Violet);
collection.Add(Brushes.Black);
- Assert.Equal(Brushes.Transparent, At(0).Foreground);
- Assert.Equal(Brushes.Blue, At(1).Foreground);
- Assert.Equal(Brushes.Transparent, At(2).Foreground);
- Assert.Equal(Brushes.Black, At(3).Foreground);
+ Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
}
}
diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs
index a70b3c9f29..8a8e46fc4b 100644
--- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs
+++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs
@@ -196,7 +196,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
- public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
+ public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
{
Border b1;
var contentControl = new ContentControl();
@@ -204,7 +204,7 @@ namespace Avalonia.Styling.UnitTests
var target = default(Selector).NthChild(1, 0);
- Assert.False(await target.Match(b1).Activator!.Take(1));
+ Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1));
}
[Fact]
diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs
index ed88106295..8d9d490724 100644
--- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs
+++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs
@@ -195,7 +195,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
- public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
+ public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
{
Border b1;
var contentControl = new ContentControl();
@@ -203,7 +203,7 @@ namespace Avalonia.Styling.UnitTests
var target = default(Selector).NthLastChild(1, 0);
- Assert.False(await target.Match(b1).Activator!.Take(1));
+ Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1));
}
[Fact]