diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
index a841803ee1..ef43720537 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
+++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
@@ -49,15 +49,6 @@ namespace Avalonia.PropertyStore
_uncommon = new() { _hasDataValidation = true };
}
- public bool HasValue
- {
- get
- {
- Start(produceValue: false);
- return _hasValue;
- }
- }
-
public bool IsSubscribed => _subscription is not null;
public AvaloniaProperty Property { get; }
AvaloniaProperty IValueEntry.Property => Property;
@@ -70,6 +61,12 @@ namespace Avalonia.PropertyStore
BindingCompleted();
}
+ public bool HasValue()
+ {
+ Start(produceValue: false);
+ return _hasValue;
+ }
+
public TValue GetValue()
{
Start(produceValue: false);
diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry.cs b/src/Avalonia.Base/PropertyStore/IValueEntry.cs
index 5898bef491..7ac7e83276 100644
--- a/src/Avalonia.Base/PropertyStore/IValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/IValueEntry.cs
@@ -8,13 +8,16 @@ namespace Avalonia.PropertyStore
///
internal interface IValueEntry
{
- bool HasValue { get; }
-
///
/// Gets the property that this value applies to.
///
AvaloniaProperty Property { get; }
+ ///
+ /// Checks whether the entry has a value, starting the entry if necessary.
+ ///
+ bool HasValue();
+
///
/// Gets the value associated with the entry.
///
diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
index 16b96eff5d..2d136fb1e5 100644
--- a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
@@ -19,13 +19,13 @@ namespace Avalonia.PropertyStore
}
public StyledProperty Property { get; }
- public bool HasValue => true;
AvaloniaProperty IValueEntry.Property => Property;
public void Unsubscribe() { }
public void Dispose() => _owner.OnEntryDisposed(this);
+ bool IValueEntry.HasValue() => true;
object? IValueEntry.GetValue() => _value;
T IValueEntry.GetValue() => _value;
diff --git a/src/Avalonia.Base/PropertyStore/ValueFrame.cs b/src/Avalonia.Base/PropertyStore/ValueFrame.cs
index 7a9d1bb13a..8ef68c651c 100644
--- a/src/Avalonia.Base/PropertyStore/ValueFrame.cs
+++ b/src/Avalonia.Base/PropertyStore/ValueFrame.cs
@@ -27,7 +27,7 @@ namespace Avalonia.PropertyStore
}
public int EntryCount => _index.Count;
- public bool IsActive => GetIsActive(out _);
+ public bool IsActive() => GetIsActive(out _);
public ValueStore? Owner => !_isShared ? _owner :
throw new AvaloniaInternalException("Cannot get owner for shared ValueFrame");
public BindingPriority Priority { get; }
diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs
index 2047f4d2d0..85adee2386 100644
--- a/src/Avalonia.Base/PropertyStore/ValueStore.cs
+++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs
@@ -843,7 +843,7 @@ namespace Avalonia.PropertyStore
// evaluated last as it can cause bindings to be subscribed.
if (foundEntry &&
HasHigherPriority(entry!, priority, current, changedValueEntry) &&
- entry!.HasValue)
+ entry!.HasValue())
{
if (current is not null)
{
@@ -911,7 +911,7 @@ namespace Avalonia.PropertyStore
{
var frame = _frames[i];
- if (!frame.IsActive)
+ if (!frame.IsActive())
continue;
var priority = frame.Priority;
@@ -927,7 +927,7 @@ namespace Avalonia.PropertyStore
if (!HasHigherPriority(entry, priority, effectiveValue, changedValueEntry))
continue;
- if (!entry.HasValue)
+ if (!entry.HasValue())
continue;
if (effectiveValue is not null)
diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
index 8fe0bb2537..888a92fcdf 100644
--- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
+++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
@@ -1,4 +1,5 @@
-using Avalonia.LogicalTree;
+using System.Collections.Generic;
+using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators
{
@@ -12,7 +13,7 @@ namespace Avalonia.Styling.Activators
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
- private int? _index;
+ private int _index = -1;
public NthChildActivator(
ILogical control,
@@ -28,7 +29,7 @@ namespace Avalonia.Styling.Activators
protected override bool EvaluateIsActive()
{
- var index = _index ?? _provider.GetChildIndex(_control);
+ var index = _index >= 0 ? _index : _provider.GetChildIndex(_control);
return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch;
}
@@ -50,26 +51,25 @@ namespace Avalonia.Styling.Activators
// 3. We're a reversed (nth-last-child) selector and total count has changed
switch (e.Action)
{
- // 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:
+ // 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
+ // and use the value from the event subscription where possible 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.
+ // the process of realizing an element of a virtualized list; in this case there may
+ // be more than one `nth-child` style on a the list item and when the other is
+ // re-evaluated calling GetChildIndex may not return the correct index as the element
+ // isn't yet realized.
case ChildIndexChangedAction.ChildIndexChanged when e.Child == _control:
- _index = e.Index;
+ _index = e.Index >= 0 ? e.Index : _provider.GetChildIndex(_control);
ReevaluateIsActive();
- _index = null;
break;
case ChildIndexChangedAction.ChildIndexesReset:
case ChildIndexChangedAction.TotalCountChanged when _reversed:
- _index = null;
+ _index = _provider.GetChildIndex(_control);
ReevaluateIsActive();
break;
}
diff --git a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
index 7604c26244..d9a2f55da9 100644
--- a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
+++ b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
@@ -15,9 +15,9 @@ namespace Avalonia.Styling
Property = property;
}
- public bool HasValue => true;
public AvaloniaProperty Property { get; }
+ public bool HasValue() => true;
public object? GetValue() => _value ??= _template.Build();
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs
index 1ac26c79ec..f31394d6f7 100644
--- a/src/Avalonia.Base/Styling/Setter.cs
+++ b/src/Avalonia.Base/Styling/Setter.cs
@@ -58,7 +58,6 @@ namespace Avalonia.Styling
}
}
- bool IValueEntry.HasValue => true;
AvaloniaProperty IValueEntry.Property => EnsureProperty();
public override string ToString() => $"Setter: {Property} = {Value}";
@@ -88,6 +87,7 @@ namespace Avalonia.Styling
return this;
}
+ bool IValueEntry.HasValue() => true;
object? IValueEntry.GetValue() => Value;
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
diff --git a/tests/Avalonia.Base.UnitTests/Styling/TestSelectors.cs b/tests/Avalonia.Base.UnitTests/Styling/TestSelectors.cs
deleted file mode 100644
index bd60275d50..0000000000
--- a/tests/Avalonia.Base.UnitTests/Styling/TestSelectors.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-using Avalonia.Styling;
-
-namespace Avalonia.Base.UnitTests.Styling
-{
- public static class TestSelectors
- {
- public static Selector SubscribeCheck(this Selector selector)
- {
- throw new NotImplementedException();
- //return new Selector(
- // selector,
- // control => new SelectorMatch(((TestControlBase)control).SubscribeCheckObservable),
- // "");
- }
- }
-}
diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
index d0e139bedf..b61131a6ee 100644
--- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
@@ -843,6 +843,64 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, panel.VisualChildren.Count);
}
+ [Fact]
+ public void Alternating_Backgrounds_Should_Be_Correct_After_Scrolling()
+ {
+ // Issue #12381.
+ static void AssertColors(VirtualizingStackPanel target)
+ {
+ var containers = target.GetRealizedContainers()!
+ .Cast()
+ .ToList();
+
+ for (var i = target.FirstRealizedIndex; i <= target.LastRealizedIndex; i++)
+ {
+ var container = Assert.IsType(target.ContainerFromIndex(i));
+ var expectedBackground = i % 2 == 0 ? Colors.Green : Colors.Red;
+ var brush = Assert.IsAssignableFrom(container.Background);
+
+ Assert.Equal(expectedBackground, brush.Color);
+ }
+ }
+
+ using var app = App();
+ var styles = new[]
+ {
+ new Style(x => x.OfType())
+ {
+ Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.White) },
+ },
+ new Style(x => x.OfType().NthChild(2, 1))
+ {
+ Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Green) },
+ },
+ new Style(x => x.OfType().NthChild(2, 0))
+ {
+ Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
+ },
+ };
+ var (target, scroll, itemsControl) = CreateUnrootedTarget();
+
+ // We need to display an odd number of items to reproduce the issue.
+ var root = CreateRoot(itemsControl, clientSize: new(100, 90), styles: styles);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ var containers = target.GetRealizedContainers()!
+ .Cast()
+ .ToList();
+
+ Assert.Equal(0, target.FirstRealizedIndex);
+ Assert.Equal(8, target.LastRealizedIndex);
+ AssertColors(target);
+
+ scroll.Offset = new Vector(0, 10);
+ target.UpdateLayout();
+
+ Assert.Equal(1, target.FirstRealizedIndex);
+ Assert.Equal(9, target.LastRealizedIndex);
+ AssertColors(target);
+ }
+
[Fact]
public void Inserting_Item_Before_Viewport_Preserves_FirstRealizedIndex()
{
@@ -925,7 +983,7 @@ namespace Avalonia.Controls.UnitTests
where T : ItemsControl, new()
{
var (target, scroll, itemsControl) = CreateUnrootedTarget(items, itemTemplate);
- var root = CreateRoot(itemsControl, styles);
+ var root = CreateRoot(itemsControl, styles: styles);
root.LayoutManager.ExecuteInitialLayoutPass();
@@ -964,10 +1022,13 @@ namespace Avalonia.Controls.UnitTests
return (target, scroll, itemsControl);
}
- private static TestRoot CreateRoot(Control? child, IEnumerable