From cf0688565a4c1044d5361380d4388ae883838bbd Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 5 Jun 2022 17:34:34 +0200 Subject: [PATCH 01/99] Refactor value storage to reduce overhead and memory allocations. --- src/Avalonia.Base/Avalonia.Base.csproj | 1 + src/Avalonia.Base/AvaloniaObject.cs | 16 + src/Avalonia.Base/StyledElement.cs | 18 +- .../Utilities/AvaloniaPropertyValueStore.cs | 236 +++++++----- src/Avalonia.Base/ValueStore.cs | 12 +- .../Avalonia.Benchmarks.csproj | 3 + .../Base/ValueStoreAddRemoveBenchmarks.cs | 351 ++++++++++++++++++ .../Layout/ControlsBenchmark.cs | 33 ++ 8 files changed, 561 insertions(+), 109 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Base/ValueStoreAddRemoveBenchmarks.cs diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 16a91b10d6..15f3b67643 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 1f14ddede4..240ce80825 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -25,6 +25,7 @@ namespace Avalonia private List? _inheritanceChildren; private ValueStore? _values; private bool _batchUpdate; + private bool _isInitializing; /// /// Initializes a new instance of the class. @@ -126,6 +127,9 @@ namespace Avalonia { _values = new ValueStore(this); + _values.IsInitializing = _isInitializing; + _isInitializing = false; + if (_batchUpdate) _values.BeginBatchUpdate(); } @@ -493,6 +497,18 @@ namespace Avalonia _batchUpdate = false; _values?.EndBatchUpdate(); } + + internal void SetValueStoreIsInitializing(bool isInitializing) + { + if (_values is null) + { + _isInitializing = isInitializing; + + return; + } + + _values.IsInitializing = isInitializing; + } /// internal void AddInheritanceChild(AvaloniaObject child) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index f98d2cdbcc..b19ef85cfa 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -306,6 +306,11 @@ namespace Avalonia public virtual void BeginInit() { ++_initCount; + + if (_initCount == 1) + { + SetValueStoreIsInitializing(true); + } } /// @@ -316,10 +321,15 @@ namespace Avalonia throw new InvalidOperationException("BeginInit was not called."); } - if (--_initCount == 0 && _logicalRoot != null) + if (--_initCount == 0) { - ApplyStyling(); - InitializeIfNeeded(); + if (_logicalRoot is not null) + { + ApplyStyling(); + InitializeIfNeeded(); + } + + SetValueStoreIsInitializing(false); } } @@ -337,11 +347,13 @@ namespace Avalonia try { BeginBatchUpdate(); + SetValueStoreIsInitializing(true); AvaloniaLocator.Current.GetService()?.ApplyStyles(this); } finally { EndBatchUpdate(); + SetValueStoreIsInitializing(false); } _styled = true; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index cbe3771577..9dbe0765de 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Avalonia.Utilities @@ -8,166 +7,197 @@ namespace Avalonia.Utilities /// Stores values with as key. /// /// Stored value type. - internal sealed class AvaloniaPropertyValueStore + internal struct AvaloniaPropertyValueStore { - // The last item in the list is always int.MaxValue. - private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } }; - - private Entry[] _entries; + private Entry[]? _entries; + private int _entryCount; public AvaloniaPropertyValueStore() { - _entries = s_emptyEntries; + _entries = null; + _entryCount = 0; + IsInitializing = false; + InitialSize = 4; } - public int Count => _entries.Length - 1; - public TValue this[int index] => _entries[index].Value; + public int Count => _entryCount; + + public bool IsInitializing { get; set; } + + public int InitialSize { get; set; } + + public TValue this[int index] => _entries![index].Value; - private (int, bool) TryFindEntry(int propertyId) + private EntryIndex LookupEntry(int propertyId) { - if (_entries.Length <= 12) + int checkIndex; + int iLo = 0; + int iHi = _entryCount; + + if (iHi <= 0) { - // For small lists, we use an optimized linear search. Since the last item in the list - // is always int.MaxValue, we can skip a conditional branch in each iteration. - // By unrolling the loop, we can skip another unconditional branch in each iteration. - - if (_entries[0].PropertyId >= propertyId) - return (0, _entries[0].PropertyId == propertyId); - if (_entries[1].PropertyId >= propertyId) - return (1, _entries[1].PropertyId == propertyId); - if (_entries[2].PropertyId >= propertyId) - return (2, _entries[2].PropertyId == propertyId); - if (_entries[3].PropertyId >= propertyId) - return (3, _entries[3].PropertyId == propertyId); - if (_entries[4].PropertyId >= propertyId) - return (4, _entries[4].PropertyId == propertyId); - if (_entries[5].PropertyId >= propertyId) - return (5, _entries[5].PropertyId == propertyId); - if (_entries[6].PropertyId >= propertyId) - return (6, _entries[6].PropertyId == propertyId); - if (_entries[7].PropertyId >= propertyId) - return (7, _entries[7].PropertyId == propertyId); - if (_entries[8].PropertyId >= propertyId) - return (8, _entries[8].PropertyId == propertyId); - if (_entries[9].PropertyId >= propertyId) - return (9, _entries[9].PropertyId == propertyId); - if (_entries[10].PropertyId >= propertyId) - return (10, _entries[10].PropertyId == propertyId); + return new EntryIndex(0, found: false); } - else + + // Do a binary search to find the value + while (iHi - iLo > 3) { - int low = 0; - int high = _entries.Length; - int id; + int iPv = (iHi + iLo) / 2; + checkIndex = _entries![iPv].PropertyId; - while (high - low > 3) + if (propertyId == checkIndex) { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; - - if (propertyId == id) - return (pivot, true); - - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; + return new EntryIndex(iPv, found: true); } - do + if (propertyId <= checkIndex) + { + iHi = iPv; + } + else { - id = _entries[low].PropertyId; + iLo = iPv + 1; + } + } - if (id == propertyId) - return (low, true); + // Now we only have three values to search; switch to a linear search + do + { + checkIndex = _entries![iLo].PropertyId; - if (id > propertyId) - break; + if (checkIndex == propertyId) + { + return new EntryIndex(iLo, found: true); + } - ++low; + if (checkIndex > propertyId) + { + // we've gone past the targetIndex - return not found + break; } - while (low < high); - } - return (0, false); + iLo++; + } while (iLo < iHi); + + return new EntryIndex(iLo, found: false); } public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) + var entryIndex = LookupEntry(property.Id); + + if (!entryIndex.Found) { value = default; return false; } - value = _entries[index].Value; + value = _entries![entryIndex.Index].Value; + return true; } - public void AddValue(AvaloniaProperty property, TValue value) + private void InsertEntry(Entry entry, int entryIndex) { - Entry[] entries = new Entry[_entries.Length + 1]; - - for (int i = 0; i < _entries.Length; ++i) + if (_entryCount > 0) { - if (_entries[i].PropertyId > property.Id) + if (_entryCount == _entries!.Length) { - if (i > 0) + // We want to have more aggressive resizing when initializing. + var growthFactor = IsInitializing ? 2.0 : 1.2; + + var newSize = (int)(_entryCount * growthFactor); + + if (newSize == _entryCount) { - Array.Copy(_entries, 0, entries, 0, i); + newSize++; } - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; + var destEntries = new Entry[newSize]; + + Array.Copy(_entries, 0, destEntries, 0, entryIndex); + + destEntries[entryIndex] = entry; + + Array.Copy(_entries, entryIndex, destEntries, entryIndex + 1, _entryCount - entryIndex); + + _entries = destEntries; } + else + { + Array.Copy( + _entries, + entryIndex, + _entries, + entryIndex + 1, + _entryCount - entryIndex); + + _entries[entryIndex] = entry; + } + } + else + { + if (_entries is null) + { + _entries = new Entry[InitialSize]; + } + + _entries[0] = entry; } - _entries = entries; + _entryCount++; + } + + public void AddValue(AvaloniaProperty property, TValue value) + { + var propertyId = property.Id; + var index = LookupEntry(propertyId); + + InsertEntry(new Entry(propertyId, value), index.Index); } public void SetValue(AvaloniaProperty property, TValue value) { - _entries[TryFindEntry(property.Id).Item1].Value = value; + var propertyId = property.Id; + var entryIndex = LookupEntry(propertyId); + + _entries![entryIndex.Index] = new Entry(propertyId, value); } public void Remove(AvaloniaProperty property) { - var (index, found) = TryFindEntry(property.Id); + var entry = LookupEntry(property.Id); - if (found) - { - var newLength = _entries.Length - 1; - - // Special case - one element left means that value store is empty so we can just reuse our "empty" array. - if (newLength == 1) - { - _entries = s_emptyEntries; - - return; - } - - var entries = new Entry[newLength]; + if (!entry.Found) return; + + Array.Copy(_entries!, entry.Index + 1, _entries!, entry.Index, _entryCount - entry.Index - 1); - int ix = 0; + _entryCount--; + _entries![_entryCount] = default; + } - for (int i = 0; i < _entries.Length; ++i) - { - if (i != index) - { - entries[ix++] = _entries[i]; - } - } + private readonly struct EntryIndex + { + public readonly int Index; + public readonly bool Found; - _entries = entries; + public EntryIndex(int index, bool found) + { + Index = index; + Found = found; } } - private struct Entry + private readonly struct Entry { - internal int PropertyId; - internal TValue Value; + public readonly int PropertyId; + public readonly TValue Value; + + public Entry(int propertyId, TValue value) + { + PropertyId = propertyId; + Value = value; + } } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 69c644dff9..664fe6c820 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -24,7 +24,7 @@ namespace Avalonia internal class ValueStore { private readonly AvaloniaObject _owner; - private readonly AvaloniaPropertyValueStore _values; + private AvaloniaPropertyValueStore _values; private BatchUpdate? _batchUpdate; public ValueStore(AvaloniaObject owner) @@ -33,6 +33,12 @@ namespace Avalonia _values = new AvaloniaPropertyValueStore(); } + public bool IsInitializing + { + get => _values.IsInitializing; + set => _values.IsInitializing = value; + } + public void BeginBatchUpdate() { _batchUpdate ??= new BatchUpdate(this); @@ -381,7 +387,7 @@ namespace Avalonia private class BatchUpdate { - private ValueStore _owner; + private readonly ValueStore _owner; private List? _notifications; private int _batchUpdateCount; private int _iterator = -1; @@ -408,7 +414,7 @@ namespace Avalonia if (--_batchUpdateCount > 0) return false; - var values = _owner._values; + ref var values = ref _owner._values; // First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed // but notifications will still not be raised because the owner ValueStore will still have a reference diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 5d17808e0c..7afc0c569f 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -5,6 +5,9 @@ Exe false + + + diff --git a/tests/Avalonia.Benchmarks/Base/ValueStoreAddRemoveBenchmarks.cs b/tests/Avalonia.Benchmarks/Base/ValueStoreAddRemoveBenchmarks.cs new file mode 100644 index 0000000000..9397ea79b4 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/ValueStoreAddRemoveBenchmarks.cs @@ -0,0 +1,351 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Utilities; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base; + +// TODO: Remove after review together with related benchmark code. +internal sealed class AvaloniaPropertyValueStoreOld +{ + // The last item in the list is always int.MaxValue. + private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } }; + + private Entry[] _entries; + + public AvaloniaPropertyValueStoreOld() + { + _entries = s_emptyEntries; + } + + public int Count => _entries.Length - 1; + public TValue this[int index] => _entries[index].Value; + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 12) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) + return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) + return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) + return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) + return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) + return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) + return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) + return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) + return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) + return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) + return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) + return (10, _entries[10].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + + return (0, false); + } + + public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = default; + return false; + } + + value = _entries[index].Value; + return true; + } + + public void AddValue(AvaloniaProperty property, TValue value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + public void SetValue(AvaloniaProperty property, TValue value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + public void Remove(AvaloniaProperty property) + { + var (index, found) = TryFindEntry(property.Id); + + if (found) + { + var newLength = _entries.Length - 1; + + // Special case - one element left means that value store is empty so we can just reuse our "empty" array. + if (newLength == 1) + { + _entries = s_emptyEntries; + + return; + } + + var entries = new Entry[newLength]; + + int ix = 0; + + for (int i = 0; i < _entries.Length; ++i) + { + if (i != index) + { + entries[ix++] = _entries[i]; + } + } + + _entries = entries; + } + } + + private struct Entry + { + internal int PropertyId; + internal TValue Value; + } +} + +internal class MockProperty : StyledProperty +{ + public MockProperty([JetBrains.Annotations.NotNull] string name) : base(name, typeof(object), new StyledPropertyMetadata()) + { + } +} + +internal static class MockProperties +{ + public static readonly AvaloniaProperty[] LinearProperties; + public static readonly AvaloniaProperty[] ShuffledProperties; + + static MockProperties() + { + LinearProperties = new AvaloniaProperty[32]; + ShuffledProperties = new AvaloniaProperty[32]; + + for (int i = 0; i < LinearProperties.Length; i++) + { + LinearProperties[i] = ShuffledProperties[i] = new MockProperty($"Property#{i}"); + } + + Shuffle(ShuffledProperties, 42); + } + + private static void Shuffle (T[] array, int seed) + { + var rng = new Random(seed); + + int n = array.Length; + while (n > 1) + { + int k = rng.Next(n--); + T temp = array[n]; + array[n] = array[k]; + array[k] = temp; + } + } +} + +[MemoryDiagnoser] +public class ValueStoreLookupBenchmarks +{ + [Params(2, 6, 10, 20, 30)] + public int PropertyCount; + + [Params(false, true)] + public bool UseShuffledProperties; + + public AvaloniaProperty[] Properties => UseShuffledProperties ? MockProperties.ShuffledProperties : MockProperties.LinearProperties; + + private AvaloniaPropertyValueStore _store; + private AvaloniaPropertyValueStoreOld _oldStore; + + [GlobalSetup] + public void GlobalSetup() + { + _store = new AvaloniaPropertyValueStore(); + _oldStore = new AvaloniaPropertyValueStoreOld(); + + for (int i = 0; i < PropertyCount; i++) + { + _store.AddValue(Properties[i], null); + _oldStore.AddValue(Properties[i], null); + } + } + + [Benchmark] + public void LookupProperties() + { + for (int i = 0; i < PropertyCount; i++) + { + _store.TryGetValue(Properties[i], out _); + } + } + + [Benchmark] public void LookupProperties_Old() + { + for (int i = 0; i < PropertyCount; i++) + { + _oldStore.TryGetValue(Properties[i], out _); + } + } +} + +[MemoryDiagnoser] +public class ValueStoreAddRemoveBenchmarks +{ + [Params(2, 6, 10, 20, 30)] + public int PropertyCount; + + [Params(false, true)] + public bool UseShuffledProperties; + + public AvaloniaProperty[] Properties => UseShuffledProperties ? MockProperties.ShuffledProperties : MockProperties.LinearProperties; + + [Benchmark] + [Arguments(false)] + [Arguments(true)] + public void AddValue(bool isInitializing) + { + var store = new AvaloniaPropertyValueStore { IsInitializing = isInitializing }; + + for (int i = 0; i < PropertyCount; i++) + { + store.AddValue(Properties[i], null); + } + } + + [Benchmark] + [Arguments(false)] + [Arguments(true)] + public void AddAndRemoveValue(bool isInitializing) + { + var store = new AvaloniaPropertyValueStore { IsInitializing = isInitializing }; + + for (int i = 0; i < PropertyCount; i++) + { + store.AddValue(Properties[i], null); + } + + for (int i = PropertyCount - 1; i >= 0; i--) + { + store.Remove(Properties[i]); + } + } + + [Benchmark] + [Arguments(false)] + [Arguments(true)] + public void AddAndRemoveValueInterleaved(bool isInitializing) + { + var store = new AvaloniaPropertyValueStore { IsInitializing = isInitializing }; + + for (int i = 0; i < PropertyCount; i++) + { + store.AddValue(Properties[i], null); + store.Remove(Properties[i]); + } + } + + [Benchmark] + public void AddValue_Old() + { + var store = new AvaloniaPropertyValueStoreOld(); + + for (int i = 0; i < PropertyCount; i++) + { + store.AddValue(Properties[i], null); + } + } + + [Benchmark] + public void AddAndRemoveValue_Old() + { + var store = new AvaloniaPropertyValueStoreOld(); + + for (int i = 0; i < PropertyCount; i++) + { + store.AddValue(Properties[i], null); + } + + for (int i = PropertyCount - 1; i >= 0; i--) + { + store.Remove(Properties[i]); + } + } + + [Benchmark] + public void AddAndRemoveValueInterleaved_Old() + { + var store = new AvaloniaPropertyValueStoreOld(); + + for (int i = 0; i < PropertyCount; i++) + { + store.AddValue(Properties[i], null); + store.Remove(Properties[i]); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs index 3493dd0f53..e71330f8bd 100644 --- a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs @@ -39,6 +39,39 @@ namespace Avalonia.Benchmarks.Layout _root.LayoutManager.ExecuteLayoutPass(); } + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateControl() + { + var control = new Control(); + + _root.Child = control; + + _root.LayoutManager.ExecuteLayoutPass(); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateDecorator() + { + var control = new Decorator(); + + _root.Child = control; + + _root.LayoutManager.ExecuteLayoutPass(); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateScrollViewer() + { + var control = new ScrollViewer(); + + _root.Child = control; + + _root.LayoutManager.ExecuteLayoutPass(); + } + [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public void CreateButton() From 71785b73d8f08be870cfe59b3e96e75f7511d5f7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 19 Jul 2022 10:37:00 +0200 Subject: [PATCH 02/99] Initial refactor of AvaloniaObject value store. Most (but not all) tests passing, all features mostly implemented exception coercion. --- src/Avalonia.Base/Animation/Animatable.cs | 2 +- .../Animation/AnimationInstance`1.cs | 2 +- src/Avalonia.Base/Avalonia.Base.csproj | 1 + src/Avalonia.Base/AvaloniaObject.cs | 440 +++----- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 18 +- src/Avalonia.Base/AvaloniaProperty.cs | 24 +- .../AvaloniaPropertyChangedEventArgs.cs | 24 +- .../AvaloniaPropertyChangedEventArgs`1.cs | 28 +- src/Avalonia.Base/CollectionPolyfills.cs | 33 + src/Avalonia.Base/Data/BindingPriority.cs | 5 + src/Avalonia.Base/Data/BindingValue.cs | 29 + src/Avalonia.Base/DirectPropertyBase.cs | 54 +- src/Avalonia.Base/IStyledPropertyAccessor.cs | 7 + .../PropertyStore/BindingEntry.cs | 191 ++-- .../PropertyStore/BindingEntry`1.cs | 169 ++++ .../PropertyStore/ConstantValueEntry.cs | 82 -- .../PropertyStore/DictionaryPool.cs | 25 + .../PropertyStore/EffectiveValue.cs | 117 +++ .../PropertyStore/EffectiveValue`1.cs | 220 ++++ .../PropertyStore/IBatchUpdate.cs | 8 - .../PropertyStore/IPriorityValueEntry.cs | 18 - src/Avalonia.Base/PropertyStore/IValue.cs | 28 - .../PropertyStore/IValueEntry.cs | 42 + .../PropertyStore/IValueEntry`1.cs | 33 + .../PropertyStore/IValueFrame.cs | 56 ++ .../PropertyStore/ImmediateValueEntry.cs | 44 + .../PropertyStore/ImmediateValueFrame.cs | 60 ++ .../PropertyStore/InheritanceFrame.cs | 34 + .../LocalValueBindingObserver.cs | 59 ++ .../PropertyStore/LocalValueEntry.cs | 41 - .../LocalValueUntypedBindingObserver.cs | 61 ++ .../PropertyStore/LoggingUtils.cs | 61 ++ .../PropertyStore/PriorityValue.cs | 326 ------ .../PropertyStore/UntypedBindingEntry.cs | 163 +++ .../PropertyStore/UntypedValueUtils.cs | 37 + .../PropertyStore/ValueFrameBase.cs | 54 + src/Avalonia.Base/PropertyStore/ValueOwner.cs | 45 - src/Avalonia.Base/PropertyStore/ValueStore.cs | 948 ++++++++++++++++++ src/Avalonia.Base/StyledElement.cs | 130 +-- src/Avalonia.Base/StyledPropertyBase.cs | 84 +- .../Styling/Activators/AndActivator.cs | 17 + .../Styling/Activators/IStyleActivator.cs | 5 + .../Styling/Activators/NotActivator.cs | 1 + .../Styling/Activators/NthChildActivator.cs | 8 +- .../Styling/Activators/OrActivator.cs | 17 + .../Activators/PropertyEqualsActivator.cs | 11 +- .../Styling/Activators/StyleActivatorBase.cs | 4 +- .../Styling/Activators/StyleClassActivator.cs | 8 +- src/Avalonia.Base/Styling/ControlTheme.cs | 1 + .../DirectPropertySetterBindingInstance.cs | 6 + .../Styling/DirectPropertySetterInstance.cs | 12 + src/Avalonia.Base/Styling/ISetter.cs | 6 +- src/Avalonia.Base/Styling/ISetterInstance.cs | 34 +- src/Avalonia.Base/Styling/IStyleInstance.cs | 19 +- src/Avalonia.Base/Styling/IStyleable.cs | 20 - .../Styling/PropertySetterBindingInstance.cs | 197 +--- .../Styling/PropertySetterTemplateInstance.cs | 121 +-- src/Avalonia.Base/Styling/Setter.cs | 80 +- src/Avalonia.Base/Styling/Style.cs | 26 + src/Avalonia.Base/Styling/StyleBase.cs | 22 +- src/Avalonia.Base/Styling/StyleInstance.cs | 143 +-- .../Utilities/AvaloniaPropertyValueStore.cs | 9 +- src/Avalonia.Base/ValueStore.cs | 507 ---------- src/Avalonia.Base/Visual.cs | 33 +- .../Primitives/TemplatedControl.cs | 10 +- .../Animation/AnimatableTests.cs | 22 +- .../AvaloniaObjectTests_BatchUpdate.cs | 695 ------------- .../AvaloniaObjectTests_Binding.cs | 372 ++++--- .../AvaloniaObjectTests_GetValue.cs | 31 +- .../AvaloniaObjectTests_Inheritance.cs | 120 ++- .../AvaloniaObjectTests_OnPropertyChanged.cs | 51 +- .../AvaloniaObjectTests_Validation.cs | 28 +- .../AvaloniaPropertyTests.cs | 16 +- .../PriorityValueTests.cs | 314 ------ .../PropertyStore/ValueStoreTests_Frames.cs | 126 +++ .../Styling/SetterTests.cs | 319 ++++-- .../Styling/StyleTests.cs | 92 +- .../Styling/StyledElementTests.cs | 67 +- .../Avalonia.Benchmarks.csproj | 1 + .../Styling/ControlTheme_Apply.cs | 147 --- .../Styling/Style_Apply.cs | 7 +- .../Styling/Style_ClassSelector.cs | 12 +- .../StyleTests.cs | 21 +- .../Xaml/StyleTests.cs | 2 +- 84 files changed, 3809 insertions(+), 3754 deletions(-) create mode 100644 src/Avalonia.Base/CollectionPolyfills.cs create mode 100644 src/Avalonia.Base/PropertyStore/BindingEntry`1.cs delete mode 100644 src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs create mode 100644 src/Avalonia.Base/PropertyStore/DictionaryPool.cs create mode 100644 src/Avalonia.Base/PropertyStore/EffectiveValue.cs create mode 100644 src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs delete mode 100644 src/Avalonia.Base/PropertyStore/IBatchUpdate.cs delete mode 100644 src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs delete mode 100644 src/Avalonia.Base/PropertyStore/IValue.cs create mode 100644 src/Avalonia.Base/PropertyStore/IValueEntry.cs create mode 100644 src/Avalonia.Base/PropertyStore/IValueEntry`1.cs create mode 100644 src/Avalonia.Base/PropertyStore/IValueFrame.cs create mode 100644 src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs create mode 100644 src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs create mode 100644 src/Avalonia.Base/PropertyStore/InheritanceFrame.cs create mode 100644 src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs delete mode 100644 src/Avalonia.Base/PropertyStore/LocalValueEntry.cs create mode 100644 src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs create mode 100644 src/Avalonia.Base/PropertyStore/LoggingUtils.cs delete mode 100644 src/Avalonia.Base/PropertyStore/PriorityValue.cs create mode 100644 src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs create mode 100644 src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs create mode 100644 src/Avalonia.Base/PropertyStore/ValueFrameBase.cs delete mode 100644 src/Avalonia.Base/PropertyStore/ValueOwner.cs create mode 100644 src/Avalonia.Base/PropertyStore/ValueStore.cs create mode 100644 src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs create mode 100644 src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs delete mode 100644 src/Avalonia.Base/ValueStore.cs delete mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs delete mode 100644 tests/Avalonia.Base.UnitTests/PriorityValueTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs delete mode 100644 tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index b045a32cd1..edaa76233e 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -235,7 +235,7 @@ namespace Avalonia.Animation private object? GetAnimationBaseValue(AvaloniaProperty property) { - var value = this.GetBaseValue(property, BindingPriority.LocalValue); + var value = this.GetBaseValue(property); if (value == AvaloniaProperty.UnsetValue) { diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index 52cd4b324f..0881fde988 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs @@ -229,7 +229,7 @@ namespace Avalonia.Animation private void UpdateNeutralValue() { var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified."); - var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); + var baseValue = _targetControl.GetBaseValue(property); _neutralValue = baseValue != AvaloniaProperty.UnsetValue ? (T)baseValue! : (T)_targetControl.GetValue(property)!; diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 15feed388b..b11a6027f2 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 6633eabb5d..60e7b2aeef 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -5,7 +5,6 @@ using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.PropertyStore; -using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia @@ -23,7 +22,7 @@ namespace Avalonia private PropertyChangedEventHandler? _inpcChanged; private EventHandler? _propertyChanged; private List? _inheritanceChildren; - private ValueStore? _values; + private ValueStore _values; private bool _batchUpdate; /// @@ -32,6 +31,7 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); + _values = new ValueStore(this); } /// @@ -59,7 +59,7 @@ namespace Avalonia /// /// The inheritance parent. /// - protected AvaloniaObject? InheritanceParent + protected internal AvaloniaObject? InheritanceParent { get { @@ -77,23 +77,8 @@ namespace Avalonia _inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent = value; - - var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()); - var propertiesCount = properties.Count; - - for (var i = 0; i < propertiesCount; i++) - { - var property = properties[i]; - if (valuestore?.IsSet(property) == true) - { - // If local value set there can be no change. - continue; - } - - property.RouteInheritanceParentChanged(this, oldParent); - } - _inheritanceParent?.AddInheritanceChild(this); + _values.SetInheritanceParent(oldParent, value); } } } @@ -118,24 +103,15 @@ namespace Avalonia set { this.Bind(binding.Property!, value); } } - private ValueStore Values - { - get - { - if (_values is null) - { - _values = new ValueStore(this); - - if (_batchUpdate) - _values.BeginBatchUpdate(); - } - - return _values; - } - } - + /// + /// Returns a value indicating whether the current thread is the UI thread. + /// + /// true if the current thread is the UI thread; otherwise false. public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); + /// + /// Checks that the current thread is the UI thread and throws if not. + /// public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); /// @@ -144,9 +120,9 @@ namespace Avalonia /// The property. public void ClearValue(AvaloniaProperty property) { - property = property ?? throw new ArgumentNullException(nameof(property)); - - property.RouteClearValue(this); + _ = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + _values.ClearLocalValue(property); } /// @@ -232,12 +208,7 @@ namespace Avalonia /// /// The property. /// The value. - public object? GetValue(AvaloniaProperty property) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - - return property.RouteGetValue(this); - } + public object? GetValue(AvaloniaProperty property) => property.RouteGetValue(this); /// /// Gets a value. @@ -247,10 +218,9 @@ namespace Avalonia /// The value. public T GetValue(StyledPropertyBase property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - - return GetValueOrInheritedOrDefault(property); + return _values.GetValue(property); } /// @@ -269,18 +239,10 @@ namespace Avalonia } /// - public Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority) + public Optional GetBaseValue(StyledPropertyBase property) { - property = property ?? throw new ArgumentNullException(nameof(property)); - VerifyAccess(); - - if (_values is object && - _values.TryGetValue(property, maxPriority, out var value)) - { - return value; - } - - return default; + _ = property ?? throw new ArgumentNullException(nameof(property)); + return _values.GetBaseValue(property); } /// @@ -346,26 +308,19 @@ namespace Avalonia T value, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); + _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - LogPropertySet(property, value, priority); + LogPropertySet(property, value, BindingPriority.LocalValue); if (value is UnsetValueType) { if (priority == BindingPriority.LocalValue) - { - Values.ClearLocalValue(property); - } - else - { - throw new NotSupportedException( - "Cannot set property to Unset at non-local value priority."); - } + _values.ClearLocalValue(property); } - else if (!(value is DoNothingType)) + else if (value is not DoNothingType) { - return Values.SetValue(property, value, priority); + return _values.SetValue(property, value, priority); } return null; @@ -389,6 +344,7 @@ namespace Avalonia /// /// Binds a to an observable. /// + /// The type of the property. /// The property. /// The observable. /// The priority of the binding. @@ -398,12 +354,51 @@ namespace Avalonia public IDisposable Bind( AvaloniaProperty property, IObservable source, + BindingPriority priority = BindingPriority.LocalValue) => property.RouteBind(this, source, priority); + + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public IDisposable Bind( + StyledPropertyBase property, + IObservable source, BindingPriority priority = BindingPriority.LocalValue) { property = property ?? throw new ArgumentNullException(nameof(property)); source = source ?? throw new ArgumentNullException(nameof(source)); + VerifyAccess(); - return property.RouteBind(this, source.ToBindingValue(), priority); + return _values.AddBinding(property, source, priority); + } + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public IDisposable Bind( + StyledPropertyBase property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + VerifyAccess(); + + return _values.AddBinding(property, source, priority); } /// @@ -425,7 +420,7 @@ namespace Avalonia source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); - return Values.AddBinding(property, source, priority); + return _values.AddBinding(property, source, priority); } /// @@ -469,29 +464,8 @@ namespace Avalonia /// The property. public void CoerceValue(AvaloniaProperty property) { - _values?.CoerceValue(property); - } - - public void BeginBatchUpdate() - { - if (_batchUpdate) - { - throw new InvalidOperationException("Batch update already in progress."); - } - - _batchUpdate = true; - _values?.BeginBatchUpdate(); - } - - public void EndBatchUpdate() - { - if (!_batchUpdate) - { - throw new InvalidOperationException("No batch update in progress."); - } - - _batchUpdate = false; - _values?.EndBatchUpdate(); + throw new NotImplementedException(); + ////_values?.CoerceValue(property); } /// @@ -507,98 +481,12 @@ namespace Avalonia _inheritanceChildren?.Remove(child); } - internal void InheritedPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - Optional newValue) - { - if (property.Inherits && (_values == null || !_values.IsSet(property))) - { - RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); - } - } - /// Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers() { return _propertyChanged?.GetInvocationList(); } - internal void ValueChanged(AvaloniaPropertyChangedEventArgs change) - { - var property = (StyledPropertyBase)change.Property; - - LogIfError(property, change.NewValue); - - // If the change is to the effective value of the property and no old/new value is set - // then fill in the old/new value from property inheritance/default value. We don't do - // this for non-effective value changes because these are only needed for property - // transitions, where knowing e.g. that an inherited value is active at an arbitrary - // priority isn't of any use and would introduce overhead. - if (change.IsEffectiveValueChange && !change.OldValue.HasValue) - { - change.SetOldValue(GetInheritedOrDefault(property)); - } - - if (change.IsEffectiveValueChange && !change.NewValue.HasValue) - { - change.SetNewValue(GetInheritedOrDefault(property)); - } - - if (!change.IsEffectiveValueChange || - !EqualityComparer.Default.Equals(change.OldValue.Value, change.NewValue.Value)) - { - RaisePropertyChanged(change); - - if (change.IsEffectiveValueChange) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( - this, - "{Property} changed from {$Old} to {$Value} with priority {Priority}", - property, - change.OldValue, - change.NewValue, - change.Priority); - } - } - } - - internal void Completed( - StyledPropertyBase property, - IPriorityValueEntry entry, - Optional oldValue) - { - var change = new AvaloniaPropertyChangedEventArgs( - this, - property, - oldValue, - default, - BindingPriority.Unset); - ValueChanged(change); - } - - /// - /// Called for each inherited property when the changes. - /// - /// The type of the property value. - /// The property. - /// The old inheritance parent. - internal void InheritanceParentChanged( - StyledPropertyBase property, - AvaloniaObject? oldParent) - { - var oldValue = oldParent is not null ? - oldParent.GetValueOrInheritedOrDefault(property) : - property.GetDefaultValue(GetType()); - - var newValue = GetInheritedOrDefault(property); - - if (!EqualityComparer.Default.Equals(oldValue, newValue)) - { - RaisePropertyChanged(property, oldValue, newValue); - } - } - internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property) { if (property.IsDirect) @@ -626,19 +514,23 @@ namespace Avalonia "Unset"); } + internal ValueStore GetValueStore() => _values; + internal IReadOnlyList? GetInheritanceChildren() => _inheritanceChildren; + /// - /// Logs a binding error for a property. + /// Gets a logger to which a binding warning may be written. /// /// The property that the error occurred on. - /// The binding error. - protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) + /// The binding exception, if any. + /// + /// This is overridden in to prevent logging binding errors when a + /// control is not attached to the visual tree. + /// + internal virtual ParametrizedLogger? GetBindingWarningLogger( + AvaloniaProperty property, + Exception? e) { - Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( - this, - "Error in binding to {Target}.{Property}: {Message}", - this, - property, - e.Message); + return Logger.TryGet(LogEventLevel.Warning, LogArea.Binding); } /// @@ -675,6 +567,22 @@ namespace Avalonia { } + // + /// Raises the event for a direct property. + /// + /// The property that has changed. + /// The old property value. + /// The new property value. + /// The priority of the binding that produced the value. + protected void RaisePropertyChanged( + DirectPropertyBase property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority = BindingPriority.LocalValue) + { + RaisePropertyChanged(property, oldValue, newValue, priority, true); + } + /// /// Raises the event. /// @@ -682,18 +590,43 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected internal void RaisePropertyChanged( + /// + /// Whether the notification represents a change to the effective value of the property. + /// + internal void RaisePropertyChanged( AvaloniaProperty property, Optional oldValue, BindingValue newValue, - BindingPriority priority = BindingPriority.LocalValue) + BindingPriority priority, + bool isEffectiveValue) { - RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs( - this, - property, - oldValue, - newValue, - priority)); + if (isEffectiveValue) + property.Notifying?.Invoke(this, true); + + try + { + var e = new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + newValue, + priority, + isEffectiveValue); + + OnPropertyChangedCore(e); + + if (isEffectiveValue) + { + property.NotifyChanged(e); + _propertyChanged?.Invoke(this, e); + _inpcChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name)); + } + } + finally + { + if (isEffectiveValue) + property.Notifying?.Invoke(this, false); + } } /// @@ -718,94 +651,10 @@ namespace Avalonia var old = field; field = value; - RaisePropertyChanged(property, old, value); + RaisePropertyChanged(property, old, value, BindingPriority.LocalValue, true); return true; } - private T GetInheritedOrDefault(StyledPropertyBase property) - { - if (property.Inherits && InheritanceParent is AvaloniaObject o) - { - return o.GetValueOrInheritedOrDefault(property); - } - - return property.GetDefaultValue(GetType()); - } - - private T GetValueOrInheritedOrDefault( - StyledPropertyBase property, - BindingPriority maxPriority = BindingPriority.Animation) - { - var o = this; - var inherits = property.Inherits; - var value = default(T); - - while (o != null) - { - var values = o._values; - - if (values != null - && values.TryGetValue(property, maxPriority, out value) == true) - { - return value; - } - - if (!inherits) - { - break; - } - - o = o.InheritanceParent as AvaloniaObject; - } - - return property.GetDefaultValue(GetType()); - } - - protected internal void RaisePropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - VerifyAccess(); - - if (change.IsEffectiveValueChange) - { - change.Property.Notifying?.Invoke(this, true); - } - - try - { - OnPropertyChangedCore(change); - - if (change.IsEffectiveValueChange) - { - change.Property.NotifyChanged(change); - _propertyChanged?.Invoke(this, change); - - if (_inpcChanged != null) - { - var inpce = new PropertyChangedEventArgs(change.Property.Name); - _inpcChanged(this, inpce); - } - - if (change.Property.Inherits && _inheritanceChildren != null) - { - foreach (var child in _inheritanceChildren) - { - child.InheritedPropertyChanged( - change.Property, - change.OldValue, - change.NewValue.ToOptional()); - } - } - } - } - finally - { - if (change.IsEffectiveValueChange) - { - change.Property.Notifying?.Invoke(this, false); - } - } - } - /// /// Sets the value of a direct property. /// @@ -839,7 +688,7 @@ namespace Avalonia throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); } - LogIfError(property, value); + LoggingUtils.LogIfNecessary(this, property, value); switch (value.Type) { @@ -877,29 +726,6 @@ namespace Avalonia return description?.Description ?? o.ToString() ?? o.GetType().Name; } - /// - /// Logs a message if the notification represents a binding error. - /// - /// The property being bound. - /// The binding notification. - private void LogIfError(AvaloniaProperty property, BindingValue value) - { - if (value.HasError) - { - if (value.Error is AggregateException aggregate) - { - foreach (var inner in aggregate.InnerExceptions) - { - LogBindingError(property, inner); - } - } - else - { - LogBindingError(property, value.Error!); - } - } - } - /// /// Logs a property set message. /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 134e3b2ac7..4ad47ce8c8 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -362,10 +362,8 @@ namespace Avalonia /// /// The object. /// The property. - /// The maximum priority for the value. /// - /// For styled properties, gets the value of the property if set on the object with a - /// priority equal or lower to , otherwise + /// For styled properties, gets the value of the property excluding animated values, otherwise /// . Note that this method does not return /// property values that come from inherited or default values. /// @@ -373,14 +371,13 @@ namespace Avalonia /// public static object? GetBaseValue( this IAvaloniaObject target, - AvaloniaProperty property, - BindingPriority maxPriority) + AvaloniaProperty property) { target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); if (target is AvaloniaObject ao) - return property.RouteGetBaseValue(ao, maxPriority); + return property.RouteGetBaseValue(ao); throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } @@ -389,10 +386,8 @@ namespace Avalonia /// /// The object. /// The property. - /// The maximum priority for the value. /// - /// For styled properties, gets the value of the property if set on the object with a - /// priority equal or lower to , otherwise + /// For styled properties, gets the value of the property excluding animated values, otherwise /// . Note that this method does not return property values /// that come from inherited or default values. /// @@ -400,8 +395,7 @@ namespace Avalonia /// public static Optional GetBaseValue( this IAvaloniaObject target, - AvaloniaProperty property, - BindingPriority maxPriority) + AvaloniaProperty property) { target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); @@ -410,7 +404,7 @@ namespace Avalonia { return property switch { - StyledPropertyBase styled => ao.GetBaseValue(styled, maxPriority), + StyledPropertyBase styled => ao.GetBaseValue(styled), DirectPropertyBase direct => ao.GetValue(direct), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") }; diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index fd43ced196..304325c791 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.PropertyStore; using Avalonia.Styling; using Avalonia.Utilities; @@ -455,6 +456,12 @@ namespace Avalonia return Name; } + /// + /// Creates an effective value for the property. + /// + /// The effective value owner. + internal abstract EffectiveValue CreateEffectiveValue(AvaloniaObject o); + /// /// Routes an untyped ClearValue call to a typed call. /// @@ -471,8 +478,7 @@ namespace Avalonia /// Routes an untyped GetBaseValue call to a typed call. /// /// The object instance. - /// The maximum priority for the value. - internal abstract object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority); + internal abstract object? RouteGetBaseValue(AvaloniaObject o); /// /// Routes an untyped SetValue call to a typed call. @@ -496,11 +502,19 @@ namespace Avalonia /// The priority. internal abstract IDisposable RouteBind( AvaloniaObject o, - IObservable> source, + IObservable source, BindingPriority priority); - internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent); - internal abstract ISetterInstance CreateSetterInstance(IStyleable target, object? value); + /// + /// Routes an untyped Bind call to a typed call. + /// + /// The object instance. + /// The binding source. + /// The priority. + internal abstract IDisposable RouteBind( + AvaloniaObject o, + IObservable> source, + BindingPriority priority); /// /// Overrides the metadata for the property on the specified type. diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index 45c67b9f48..a3ca25bc45 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -17,6 +17,16 @@ namespace Avalonia IsEffectiveValueChange = true; } + internal AvaloniaPropertyChangedEventArgs( + IAvaloniaObject sender, + BindingPriority priority, + bool isEffectiveValueChange) + { + Sender = sender; + Priority = priority; + IsEffectiveValueChange = isEffectiveValueChange; + } + /// /// Gets the that the property changed on. /// @@ -49,20 +59,8 @@ namespace Avalonia /// public BindingPriority Priority { get; private set; } - /// - /// Gets a value indicating whether the change represents a change to the effective value of - /// the property. - /// - /// - /// This will usually be true, except in - /// - /// which receives notifications for all changes to property values, whether a value with a higher - /// priority is present or not. When this property is false, the change that is being signaled - /// has not resulted in a change to the property value on the object. - /// - public bool IsEffectiveValueChange { get; private set; } + internal bool IsEffectiveValueChange { get; private set; } - internal void MarkNonEffectiveValue() => IsEffectiveValueChange = false; protected abstract AvaloniaProperty GetProperty(); protected abstract object? GetOldValue(); protected abstract object? GetNewValue(); diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs index 734e38596c..2c7a597537 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -21,7 +21,18 @@ namespace Avalonia Optional oldValue, BindingValue newValue, BindingPriority priority) - : base(sender, priority) + : this(sender, property, oldValue, newValue, priority, true) + { + } + + internal AvaloniaPropertyChangedEventArgs( + IAvaloniaObject sender, + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority, + bool isEffectiveValueChange) + : base(sender, priority, isEffectiveValueChange) { Property = property; OldValue = oldValue; @@ -39,28 +50,13 @@ namespace Avalonia /// /// Gets the old value of the property. /// - /// - /// When is true, returns the - /// old value of the property on the object. - /// When is false, returns - /// . - /// public new Optional OldValue { get; private set; } /// /// Gets the new value of the property. /// - /// - /// When is true, returns the - /// value of the property on the object. - /// When is false returns the - /// changed value, or if the value was removed. - /// public new BindingValue NewValue { get; private set; } - internal void SetOldValue(Optional value) => OldValue = value; - internal void SetNewValue(BindingValue value) => NewValue = value; - protected override AvaloniaProperty GetProperty() => Property; protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/CollectionPolyfills.cs b/src/Avalonia.Base/CollectionPolyfills.cs new file mode 100644 index 0000000000..b41c3a4d2c --- /dev/null +++ b/src/Avalonia.Base/CollectionPolyfills.cs @@ -0,0 +1,33 @@ +#if !NET6_0_OR_GREATER +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia +{ + internal static class CollectionPolyfills + { + public static bool Remove( + this Dictionary o, + TKey key, + [MaybeNullWhen(false)] out TValue value) + where TKey : notnull + { + if (o.TryGetValue(key, out value)) + return o.Remove(key); + return false; + } + + public static bool TryAdd(this Dictionary o, TKey key, TValue value) + where TKey : notnull + { + if (!o.ContainsKey(key)) + { + o.Add(key, value); + return true; + } + + return false; + } + } +} +#endif diff --git a/src/Avalonia.Base/Data/BindingPriority.cs b/src/Avalonia.Base/Data/BindingPriority.cs index ece64375f2..dd1654f53c 100644 --- a/src/Avalonia.Base/Data/BindingPriority.cs +++ b/src/Avalonia.Base/Data/BindingPriority.cs @@ -35,6 +35,11 @@ namespace Avalonia.Data /// A style binding. /// Style, + + /// + /// The value is inherited from an ancestor element. + /// + Inherited, /// /// The binding is uninitialized. diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 55be611083..247938dec4 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using Avalonia.Utilities; @@ -245,6 +246,34 @@ namespace Avalonia.Data }; } + public static bool operator !=(BindingValue x, Optional y) + { + if (x.HasValue != y.HasValue) + return true; + return !EqualityComparer.Default.Equals(x.Value, y.Value); + } + + public static bool operator ==(BindingValue x, Optional y) + { + if (x.HasValue != y.HasValue) + return false; + return EqualityComparer.Default.Equals(x.Value, y.Value); + } + + public static bool operator !=(Optional x, BindingValue y) + { + if (x.HasValue != y.HasValue) + return true; + return !EqualityComparer.Default.Equals(x.Value, y.Value); + } + + public static bool operator ==(Optional x, BindingValue y) + { + if (x.HasValue != y.HasValue) + return false; + return EqualityComparer.Default.Equals(x.Value, y.Value); + } + /// /// Creates a binding value from an instance of the underlying value type. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index efcb7dfecb..fcb78a9b42 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Reactive; using Avalonia.Styling; @@ -120,6 +121,11 @@ namespace Avalonia base.OverrideMetadata(type, metadata); } + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) + { + throw new InvalidOperationException("Cannot create EffectiveValue for direct property."); + } + /// internal override void RouteClearValue(AvaloniaObject o) { @@ -132,7 +138,7 @@ namespace Avalonia return o.GetValue(this); } - internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(AvaloniaObject o) { return o.GetValue(this); } @@ -161,6 +167,22 @@ namespace Avalonia return null; } + /// + /// Routes an untyped Bind call to a typed call. + /// + /// The object instance. + /// The binding source. + /// The priority. + internal override IDisposable RouteBind( + AvaloniaObject o, + IObservable source, + BindingPriority priority) + { + // TODO: this requires a double adapter, we should make AvaloniaObject + // accept an `IObservable` for direct properties directly. + return RouteBind(o, source.ToBindingValue(), priority); + } + /// internal override IDisposable RouteBind( AvaloniaObject o, @@ -170,35 +192,5 @@ namespace Avalonia var adapter = TypedBindingAdapter.Create(o, this, source); return o.Bind(this, adapter); } - - internal override void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent) - { - throw new NotSupportedException("Direct properties do not support inheritance."); - } - - internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value) - { - if (value is IBinding binding) - { - return new PropertySetterBindingInstance( - target, - this, - binding); - } - else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType)) - { - return new PropertySetterTemplateInstance( - target, - this, - template); - } - else - { - return new PropertySetterInstance( - target, - this, - (TValue)value!); - } - } } } diff --git a/src/Avalonia.Base/IStyledPropertyAccessor.cs b/src/Avalonia.Base/IStyledPropertyAccessor.cs index c4a0005f55..4cbfd7b759 100644 --- a/src/Avalonia.Base/IStyledPropertyAccessor.cs +++ b/src/Avalonia.Base/IStyledPropertyAccessor.cs @@ -15,5 +15,12 @@ namespace Avalonia /// The default value. /// object? GetDefaultValue(Type type); + + /// + /// Validates the specified property value. + /// + /// The value. + /// True if the value is valid, otherwise false. + bool ValidateValue(object? value); } } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 9a25e98a23..d95cd5ed05 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -1,154 +1,137 @@ using System; +using System.Diagnostics; +using System.Reactive.Disposables; using Avalonia.Data; -using Avalonia.Threading; namespace Avalonia.PropertyStore { - /// - /// Represents an untyped interface to . - /// - internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable + internal class BindingEntry : IValueEntry, + IObserver, + IDisposable { - void Start(bool ignoreBatchUpdate); - } - - /// - /// Stores a binding in a or . - /// - /// The property type. - internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserver> - { - private readonly AvaloniaObject _owner; - private ValueOwner _sink; + private readonly ValueFrameBase _frame; + private readonly IObservable _source; private IDisposable? _subscription; - private bool _isSubscribed; - private bool _batchUpdate; - private Optional _value; + private bool _hasValue; + private object? _value; public BindingEntry( - AvaloniaObject owner, - StyledPropertyBase property, - IObservable> source, - BindingPriority priority, - ValueOwner sink) + ValueFrameBase frame, + AvaloniaProperty property, + IObservable source) { - _owner = owner; + _frame = frame; + _source = source; Property = property; - Source = source; - Priority = priority; - _sink = sink; } - public StyledPropertyBase Property { get; } - public BindingPriority Priority { get; private set; } - public IObservable> Source { get; } - Optional IValue.GetValue() => _value.ToObject(); - - public void BeginBatchUpdate() => _batchUpdate = true; - - public void EndBatchUpdate() + public bool HasValue { - _batchUpdate = false; - - if (_sink.IsValueStore) - Start(); + get + { + StartIfNecessary(); + return _hasValue; + } } - public Optional GetValue(BindingPriority maxPriority) - { - return Priority >= maxPriority ? _value : Optional.Empty; - } + public AvaloniaProperty Property { get; } public void Dispose() { - _subscription?.Dispose(); - _subscription = null; - OnCompleted(); + Unsubscribe(); + BindingCompleted(); } - public void OnCompleted() + public object? GetValue() { - var oldValue = _value; - _value = default; - Priority = BindingPriority.Unset; - _isSubscribed = false; - _sink.Completed(Property, this, oldValue); + StartIfNecessary(); + if (!_hasValue) + throw new AvaloniaInternalException("The binding entry has no value."); + return _value!; } - public void OnError(Exception error) + public bool TryGetValue(out object? value) { - throw new NotImplementedException("BindingEntry.OnError is not implemented", error); + StartIfNecessary(); + value = _value; + return _hasValue; } - public void OnNext(BindingValue value) + public void Start() { - if (Dispatcher.UIThread.CheckAccess()) - { - UpdateValue(value); - } - else - { - // To avoid allocating closure in the outer scope we need to capture variables - // locally. This allows us to skip most of the allocations when on UI thread. - var instance = this; - var newValue = value; + Debug.Assert(_subscription is null); - Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue)); - } + // Subscription won't be set until Subscribe completes, but in the meantime we + // need to signal that we've started as Subscribe may cause a value to be produced. + _subscription = Disposable.Empty; + _subscription = _source.Subscribe(this); } - public void Start() => Start(false); + public void OnCompleted() => BindingCompleted(); + public void OnError(Exception error) => BindingCompleted(); + + public void OnNext(object? value) => SetValue(value); - public void Start(bool ignoreBatchUpdate) + public virtual void Unsubscribe() { - // We can't use _subscription to check whether we're subscribed because it won't be set - // until Subscribe has finished, which will be too late to prevent reentrancy. In addition - // don't re-subscribe to completed/disposed bindings (indicated by Unset priority). - if (!_isSubscribed && - Priority != BindingPriority.Unset && - (!_batchUpdate || ignoreBatchUpdate)) - { - _isSubscribed = true; - _subscription = Source.Subscribe(this); - } + _subscription?.Dispose(); + _subscription = null; } - public void Reparent(PriorityValue parent) => _sink = new(parent); - - public void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue) + private void ClearValue() { - owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - owner, - (AvaloniaProperty)property, - oldValue.Cast(), - newValue.Cast(), - Priority)); + if (_hasValue) + { + _hasValue = false; + _value = default; + _frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); + } } - private void UpdateValue(BindingValue value) + private void SetValue(object? value) { - if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) + if (_frame.Owner is null) + return; + + if (value is BindingNotification n) { - value = Property.GetDefaultValue(_owner.GetType()); + value = n.Value; } - if (value.Type == BindingValueType.DoNothing) + if (value == AvaloniaProperty.UnsetValue) { - return; + ClearValue(); } - - var old = _value; - - if (value.Type != BindingValueType.DataValidationError) + else if (value == BindingOperations.DoNothing) + { + // Do nothing! + } + else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) + { + if (!_hasValue || !Equals(_value, typedValue)) + { + _value = typedValue; + _hasValue = true; + _frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, typedValue); + } + } + else { - _value = value.ToOptional(); + ClearValue(); + LoggingUtils.LogInvalidValue(_frame.Owner.Owner, Property, Property.PropertyType, value); } + } - _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(_owner, Property, old, value, Priority)); + private void BindingCompleted() + { + _subscription = null; + _frame.OnBindingCompleted(this); + } + + private void StartIfNecessary() + { + if (_subscription is null) + Start(); } } } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry`1.cs b/src/Avalonia.Base/PropertyStore/BindingEntry`1.cs new file mode 100644 index 0000000000..38b1e42228 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/BindingEntry`1.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reactive.Disposables; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + internal class BindingEntry : IValueEntry, + IObserver, + IObserver>, + IDisposable + { + private readonly ValueFrameBase _frame; + private readonly object _source; + private IDisposable? _subscription; + private bool _hasValue; + private T? _value; + + public BindingEntry( + ValueFrameBase frame, + StyledPropertyBase property, + IObservable> source) + { + _frame = frame; + _source = source; + Property = property; + } + + public BindingEntry( + ValueFrameBase frame, + StyledPropertyBase property, + IObservable source) + { + _frame = frame; + _source = source; + Property = property; + } + + public bool HasValue + { + get + { + StartIfNecessary(); + return _hasValue; + } + } + + public StyledPropertyBase Property { get; } + AvaloniaProperty IValueEntry.Property => Property; + + public void Dispose() + { + Unsubscribe(); + BindingCompleted(); + } + + public T GetValue() + { + StartIfNecessary(); + if (!_hasValue) + throw new AvaloniaInternalException("The binding entry has no value."); + return _value!; + } + + public void Start() + { + Debug.Assert(_subscription is null); + + // Subscription won't be set until Subscribe completes, but in the meantime we + // need to signal that we've started as Subscribe may cause a value to be produced. + _subscription = Disposable.Empty; + + if (_source is IObservable> bv) + _subscription = bv.Subscribe(this); + else if (_source is IObservable b) + _subscription = b.Subscribe(this); + else + throw new AvaloniaInternalException("Unexpected binding source."); + } + + public bool TryGetValue([MaybeNullWhen(false)] out T value) + { + StartIfNecessary(); + value = _value; + return _hasValue; + } + + public void OnCompleted() => BindingCompleted(); + public void OnError(Exception error) => BindingCompleted(); + + public void OnNext(T value) => SetValue(value); + + public void OnNext(BindingValue value) + { + if (_frame.Owner is not null) + LoggingUtils.LogIfNecessary(_frame.Owner.Owner, Property, value); + + if (value.HasValue) + SetValue(value.Value); + else + ClearValue(); + } + + public void Unsubscribe() + { + _subscription?.Dispose(); + _subscription = null; + } + + object? IValueEntry.GetValue() + { + StartIfNecessary(); + if (!_hasValue) + throw new AvaloniaInternalException("The BindingEntry has no value."); + return _value!; + } + + bool IValueEntry.TryGetValue(out object? value) + { + StartIfNecessary(); + value = _value; + return _hasValue; + } + + private void ClearValue() + { + if (_hasValue) + { + _hasValue = false; + _value = default; + _frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); + } + } + + private void SetValue(T value) + { + if (_frame.Owner is null) + return; + + if (Property.ValidateValue?.Invoke(value) != false) + { + if (!_hasValue || !EqualityComparer.Default.Equals(_value, value)) + { + _value = value; + _hasValue = true; + _frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, value); + } + } + else + { + _frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); + } + } + + private void BindingCompleted() + { + _subscription = null; + _frame.OnBindingCompleted(this); + } + + private void StartIfNecessary() + { + if (_subscription is null) + Start(); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs deleted file mode 100644 index 4116f4abd9..0000000000 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable - { - } - - /// - /// Stores a value with a priority in a or - /// . - /// - /// The property type. - internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry - { - private ValueOwner _sink; - private Optional _value; - - public ConstantValueEntry( - StyledPropertyBase property, - T value, - BindingPriority priority, - ValueOwner sink) - { - Property = property; - _value = value; - Priority = priority; - _sink = sink; - } - - public ConstantValueEntry( - StyledPropertyBase property, - Optional value, - BindingPriority priority, - ValueOwner sink) - { - Property = property; - _value = value; - Priority = priority; - _sink = sink; - } - - public StyledPropertyBase Property { get; } - public BindingPriority Priority { get; private set; } - Optional IValue.GetValue() => _value.ToObject(); - - public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) - { - return Priority >= maxPriority ? _value : Optional.Empty; - } - - public void Dispose() - { - var oldValue = _value; - _value = default; - Priority = BindingPriority.Unset; - _sink.Completed(Property, this, oldValue); - } - - public void Reparent(PriorityValue sink) => _sink = new(sink); - public void Start() { } - - public void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue) - { - owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - owner, - (AvaloniaProperty)property, - oldValue.Cast(), - newValue.Cast(), - Priority)); - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/DictionaryPool.cs b/src/Avalonia.Base/PropertyStore/DictionaryPool.cs new file mode 100644 index 0000000000..258934b980 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/DictionaryPool.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Avalonia.PropertyStore +{ + internal static class DictionaryPool + where TKey : notnull + { + private const int MaxPoolSize = 4; + private static Stack> _pool = new(); + + public static Dictionary Get() + { + return _pool.Count == 0 ? new() : _pool.Pop(); + } + + public static void Release(Dictionary dictionary) + { + if (_pool.Count < MaxPoolSize) + { + dictionary.Clear(); + _pool.Push(dictionary); + } + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs new file mode 100644 index 0000000000..27d3f319d5 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -0,0 +1,117 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// Represents the active value for a property in a . + /// + /// + /// This class is an abstract base for the generic . + /// + internal abstract class EffectiveValue + { + /// + /// Gets the current effective value as a boxed value. + /// + public object? Value => GetBoxedValue(); + + /// + /// Gets the current effective base value as a boxed value, or + /// if not set. + /// + public object? BaseValue => GetBoxedBaseValue(); + + /// + /// Gets the priority of the current effective value. + /// + public BindingPriority Priority { get; protected set; } + + /// + /// Gets the priority of the current base value. + /// + public BindingPriority BasePriority { get; protected set; } + + /// + /// Sets the value and base value, raising + /// where necessary. + /// + /// The associated value store. + /// The property being changed. + /// The new value of the property. + /// The priority of the new value. + public abstract void SetAndRaise( + ValueStore owner, + AvaloniaProperty property, + object? value, + BindingPriority priority); + + /// + /// Sets the value and base value, raising + /// where necessary. + /// + /// The associated value store. + /// The property being changed. + /// The new value of the property. + /// The priority of the new value. + /// The new base value of the property. + /// The priority of the new base value. + public abstract void SetAndRaise( + ValueStore owner, + AvaloniaProperty property, + object? value, + BindingPriority priority, + object? baseValue, + BindingPriority basePriority); + + /// + /// Sets the value, raising + /// where necessary. + /// + /// The associated value store. + /// The value entry with the new value of the property. + /// The priority of the new value. + /// + /// This method does not set the base value. + /// + public abstract void SetAndRaise( + ValueStore owner, + IValueEntry entry, + BindingPriority priority); + + /// + /// Set the value priority, but leaves the value unchanged. + /// + public void SetPriority(BindingPriority priority) => Priority = BindingPriority.Unset; + + /// + /// Set the base value priority, but leaves the base value unchanged. + /// + public void SetBasePriority(BindingPriority priority) => BasePriority = BindingPriority.Unset; + + /// + /// Raises in response to an inherited value + /// change. + /// + /// The owner object. + /// The property being changed. + /// The old value of the property. + /// The new value of the property. + public abstract void RaiseInheritedValueChanged( + AvaloniaObject owner, + AvaloniaProperty property, + EffectiveValue? oldValue, + EffectiveValue? newValue); + + /// + /// Disposes the effective value, raising + /// where necessary. + /// + /// The associated value store. + /// The property being cleared. + public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property); + + protected abstract object? GetBoxedValue(); + protected abstract object? GetBoxedBaseValue(); + } +} diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs new file mode 100644 index 0000000000..2b3865d23d --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -0,0 +1,220 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// Represents the active value for a property in a . + /// + /// + /// Stores the active value in an 's + /// for a single property, when the value is not inherited or unset/default. + /// + internal sealed class EffectiveValue : EffectiveValue + { + private T? _baseValue; + + public EffectiveValue(T value, BindingPriority priority) + { + Value = value; + Priority = priority; + + if (priority >= BindingPriority.LocalValue && priority < BindingPriority.Inherited) + { + _baseValue = value; + BasePriority = priority; + } + else + { + _baseValue = default; + BasePriority = BindingPriority.Unset; + } + } + + /// + /// Gets the current effective value. + /// + public new T Value { get; private set; } + + public override void SetAndRaise( + ValueStore owner, + AvaloniaProperty property, + object? value, + BindingPriority priority) + { + // `value` should already have been converted to the correct type and + // validated by this point. + SetAndRaise(owner, (StyledPropertyBase)property, (T)value!, priority); + } + + public override void SetAndRaise( + ValueStore owner, + AvaloniaProperty property, + object? value, + BindingPriority priority, + object? baseValue, + BindingPriority basePriority) + { + SetAndRaise(owner, (StyledPropertyBase)property, (T)value!, priority, (T)baseValue!, basePriority); + } + + public override void SetAndRaise( + ValueStore owner, + IValueEntry entry, + BindingPriority priority) + { + var value = entry is IValueEntry typed ? typed.GetValue() : (T)entry.GetValue()!; + SetAndRaise(owner, (StyledPropertyBase)entry.Property, value, priority); + } + + /// + /// Sets the value and base value, raising + /// where necessary. + /// + /// The object on which to raise events. + /// The property being changed. + /// The new value of the property. + /// The priority of the new value. + public void SetAndRaise( + ValueStore owner, + StyledPropertyBase property, + T value, + BindingPriority priority) + { + Debug.Assert(priority < BindingPriority.Inherited); + + var oldValue = Value; + var valueChanged = false; + var baseValueChanged = false; + + if (priority <= Priority) + { + valueChanged = !EqualityComparer.Default.Equals(Value, value); + Value = value; + Priority = priority; + } + + if (priority <= BasePriority && priority >= BindingPriority.LocalValue) + { + baseValueChanged = !EqualityComparer.Default.Equals(_baseValue, value); + _baseValue = value; + BasePriority = priority; + } + + if (valueChanged) + { + owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true); + if (property.Inherits) + owner.OnInheritedEffectiveValueChanged(property, oldValue, this); + } + else if (baseValueChanged) + { + owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false); + } + } + + /// + /// Sets the value and base value, raising + /// where necessary. + /// + /// The object on which to raise events. + /// The property being changed. + /// The new value of the property. + /// The priority of the new value. + /// The new base value of the property. + /// The priority of the new base value. + public void SetAndRaise( + ValueStore owner, + StyledPropertyBase property, + T value, + BindingPriority priority, + T baseValue, + BindingPriority basePriority) + { + Debug.Assert(priority < BindingPriority.Inherited); + Debug.Assert(basePriority > BindingPriority.Animation); + + var oldValue = Value; + var valueChanged = false; + var baseValueChanged = false; + + if (!EqualityComparer.Default.Equals(Value, value)) + { + Value = value; + valueChanged = true; + } + + if (BasePriority == BindingPriority.Unset || + !EqualityComparer.Default.Equals(_baseValue, baseValue)) + { + _baseValue = value; + baseValueChanged = true; + } + + Priority = priority; + BasePriority = basePriority; + + if (valueChanged) + { + owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true); + if (property.Inherits) + owner.OnInheritedEffectiveValueChanged(property, oldValue, this); + } + else if (baseValueChanged) + { + owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false); + } + } + + public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) + { + value = _baseValue!; + return BasePriority != BindingPriority.Unset; + } + + public override void RaiseInheritedValueChanged( + AvaloniaObject owner, + AvaloniaProperty property, + EffectiveValue? oldValue, + EffectiveValue? newValue) + { + Debug.Assert(oldValue is not null || newValue is not null); + + var p = (StyledPropertyBase)property; + var o = oldValue is not null ? ((EffectiveValue)oldValue).Value : p.GetDefaultValue(owner.GetType()); + var n = newValue is not null ? ((EffectiveValue)newValue).Value : p.GetDefaultValue(owner.GetType()); + var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset; + + if (!EqualityComparer.Default.Equals(o, n)) + { + owner.RaisePropertyChanged(p, o, n, priority, true); + } + } + + public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) + { + DisposeAndRaiseUnset(owner, (StyledPropertyBase)property); + } + + public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase property) + { + var defaultValue = property.GetDefaultValue(owner.GetType()); + + if (!EqualityComparer.Default.Equals(defaultValue, Value)) + { + owner.Owner.RaisePropertyChanged(property, Value, defaultValue, BindingPriority.Unset, true); + if (property.Inherits) + owner.OnInheritedEffectiveValueDisposed(property, Value); + } + } + + protected override object? GetBoxedValue() => Value; + + protected override object? GetBoxedBaseValue() + { + return BasePriority != BindingPriority.Unset ? _baseValue : AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs b/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs deleted file mode 100644 index af4faf989c..0000000000 --- a/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Avalonia.PropertyStore -{ - internal interface IBatchUpdate - { - void BeginBatchUpdate(); - void EndBatchUpdate(); - } -} diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs deleted file mode 100644 index 45bbd0cda5..0000000000 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - internal interface IPriorityValueEntry : IValue - { - } - - /// - /// Represents an object that can act as an entry in a . - /// - /// The property type. - internal interface IPriorityValueEntry : IPriorityValueEntry, IValue - { - void Reparent(PriorityValue parent); - } -} diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs deleted file mode 100644 index b493df92e6..0000000000 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - internal interface IValue - { - BindingPriority Priority { get; } - Optional GetValue(); - void Start(); - void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue); - } - - /// - /// Represents an object that can act as an entry in a . - /// - /// The property type. - internal interface IValue : IValue - { - Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation); - } -} diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry.cs b/src/Avalonia.Base/PropertyStore/IValueEntry.cs new file mode 100644 index 0000000000..43709fe115 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueEntry.cs @@ -0,0 +1,42 @@ +using System; + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an untyped value entry in an . + /// + internal interface IValueEntry + { + bool HasValue { get; } + + /// + /// Gets the property that this value applies to. + /// + AvaloniaProperty Property { get; } + + /// + /// Gets the value associated with the entry. + /// + /// + /// The entry has no value. + /// + object? GetValue(); + + /// + /// Tries to get the value associated with the entry. + /// + /// + /// When this method returns, contains the value associated with the entry if a value is + /// present; otherwise, returns null. + /// + /// + /// true if the entry has an associated value; otherwise false. + /// + bool TryGetValue(out object? value); + + /// + /// Called when the value entry is removed from the value store. + /// + void Unsubscribe(); + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry`1.cs b/src/Avalonia.Base/PropertyStore/IValueEntry`1.cs new file mode 100644 index 0000000000..b52ca4815b --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueEntry`1.cs @@ -0,0 +1,33 @@ +namespace Avalonia.PropertyStore +{ + /// + /// Represents a typed value entry in an . + /// + internal interface IValueEntry : IValueEntry + { + /// + /// Gets the property that this value applies to. + /// + new StyledPropertyBase Property { get; } + + /// + /// Gets the value associated with the entry. + /// + /// + /// The entry has no value. + /// + new T GetValue(); + + /// + /// Tries to get the value associated with the entry. + /// + /// + /// When this method returns, contains the value associated with the entry if a value is + /// present; otherwise, returns the default value of . + /// + /// + /// true if the entry has an associated value; otherwise false. + /// + bool TryGetValue(out T? value); + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValueFrame.cs b/src/Avalonia.Base/PropertyStore/IValueFrame.cs new file mode 100644 index 0000000000..99338a4720 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueFrame.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// Represents a collection of property values in a . + /// + /// + /// A value frame is an abstraction over the following sources of values in an + /// : + /// + /// - A style + /// - Local values + /// - Animation values + /// + internal interface IValueFrame : IDisposable + { + /// + /// Gets the number of value entries in the frame. + /// + int EntryCount { get; } + + /// + /// Gets a value indicating whether the frame is active. + /// + bool IsActive { get; } + + /// + /// Gets the value store that owns the frame. + /// + ValueStore? Owner { get; } + + /// + /// Gets the frame's priority. + /// + BindingPriority Priority { get; } + + /// + /// Retreives the frame's value entry with the specified index. + /// + IValueEntry GetEntry(int index); + + /// + /// Sets the owner of the value frame. + /// + /// The new owner. + void SetOwner(ValueStore? owner); + + /// + /// Tries to retreive the frame's value entry for the specified property. + /// + bool TryGetEntry(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? entry); + } +} diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs new file mode 100644 index 0000000000..7c17917393 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs @@ -0,0 +1,44 @@ +using System; + +namespace Avalonia.PropertyStore +{ + internal class ImmediateValueEntry : IValueEntry, IDisposable + { + private readonly ImmediateValueFrame _owner; + private readonly T _value; + + public ImmediateValueEntry( + ImmediateValueFrame owner, + StyledPropertyBase property, + T value) + { + _owner = owner; + _value = value; + Property = property; + } + + public StyledPropertyBase Property { get; } + public bool HasValue => true; + AvaloniaProperty IValueEntry.Property => Property; + + public T GetValue() => _value; + + public bool TryGetValue(out T? value) + { + value = _value; + return true; + } + + public bool TryGetValue(out object? value) + { + value = _value; + return true; + } + + public void Unsubscribe() { } + + public void Dispose() => _owner.OnEntryDisposed(this); + + object? IValueEntry.GetValue() => _value; + } +} diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs new file mode 100644 index 0000000000..94d357ffe4 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// Holds values in a set by one of the SetValue or AddBinding + /// overloads with non-LocalValue priority. + /// + internal class ImmediateValueFrame : ValueFrameBase + { + public ImmediateValueFrame(BindingPriority priority) + { + Priority = priority; + } + + public override bool IsActive => true; + public override BindingPriority Priority { get; } + + public BindingEntry AddBinding( + StyledPropertyBase property, + IObservable> source) + { + var e = new BindingEntry(this, property, source); + Add(e); + return e; + } + + public BindingEntry AddBinding( + StyledPropertyBase property, + IObservable source) + { + var e = new BindingEntry(this, property, source); + Add(e); + return e; + } + + public UntypedBindingEntry AddBinding( + StyledPropertyBase property, + IObservable source) + { + var e = new UntypedBindingEntry(this, property, source); + Add(e); + return e; + } + + public IDisposable AddValue(StyledPropertyBase property, T value) + { + var e = new ImmediateValueEntry(this, property, value); + Add(e); + return e; + } + + public void OnEntryDisposed(IValueEntry value) + { + Remove(value.Property); + Owner?.OnValueEntryRemoved(this, value.Property); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/InheritanceFrame.cs b/src/Avalonia.Base/PropertyStore/InheritanceFrame.cs new file mode 100644 index 0000000000..3df42ee366 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/InheritanceFrame.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia.PropertyStore +{ + internal class InheritanceFrame : Dictionary + { + public InheritanceFrame(ValueStore owner, InheritanceFrame? parent = null) + { + Owner = owner; + Parent = parent; + } + + public ValueStore Owner { get; } + public InheritanceFrame? Parent { get; private set; } + + public bool TryGetFromThisOrAncestor(AvaloniaProperty property, [NotNullWhen(true)] out EffectiveValue? value) + { + var frame = this; + + while (frame is object) + { + if (frame.TryGetValue(property, out value)) + return true; + frame = frame.Parent; + } + + value = default; + return false; + } + + public void SetParent(InheritanceFrame? value) => Parent = value; + } +} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs new file mode 100644 index 0000000000..4dca6c0100 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -0,0 +1,59 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + internal class LocalValueBindingObserver : IObserver, + IObserver>, + IDisposable + { + private readonly ValueStore _owner; + private IDisposable? _subscription; + + public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase property) + { + _owner = owner; + Property = property; + } + + public StyledPropertyBase Property { get;} + + public void Start(IObservable source) + { + _subscription = source.Subscribe(this); + } + + public void Start(IObservable> source) + { + _subscription = source.Subscribe(this); + } + + public void Dispose() + { + _subscription?.Dispose(); + _subscription = null; + _owner.OnLocalValueBindingCompleted(Property, this); + } + + public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); + public void OnError(Exception error) => OnCompleted(); + + public void OnNext(T value) + { + if (Property.ValidateValue?.Invoke(value) != false) + _owner.SetValue(Property, value, BindingPriority.LocalValue); + else + _owner.ClearLocalValue(Property); + } + + public void OnNext(BindingValue value) + { + LoggingUtils.LogIfNecessary(_owner.Owner, Property, value); + + if (value.HasValue) + _owner.SetValue(Property, value.Value, BindingPriority.LocalValue); + else if (value.Type != BindingValueType.DataValidationError) + _owner.ClearLocalValue(Property); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs deleted file mode 100644 index 13ca69681f..0000000000 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Stores a value with local value priority in a or - /// . - /// - /// The property type. - internal class LocalValueEntry : IValue - { - private T _value; - - public LocalValueEntry(T value) => _value = value; - public BindingPriority Priority => BindingPriority.LocalValue; - Optional IValue.GetValue() => new Optional(_value); - - public Optional GetValue(BindingPriority maxPriority) - { - return BindingPriority.LocalValue >= maxPriority ? _value : Optional.Empty; - } - - public void SetValue(T value) => _value = value; - public void Start() { } - - public void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue) - { - owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - owner, - (AvaloniaProperty)property, - oldValue.Cast(), - newValue.Cast(), - BindingPriority.LocalValue)); - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs new file mode 100644 index 0000000000..8a745482e7 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -0,0 +1,61 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + internal class LocalValueUntypedBindingObserver : IObserver, + IDisposable + { + private readonly ValueStore _owner; + private IDisposable? _subscription; + + public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase property) + { + _owner = owner; + Property = property; + } + + public StyledPropertyBase Property { get; } + + public void Start(IObservable source) + { + _subscription = source.Subscribe(this); + } + + public void Dispose() + { + _subscription?.Dispose(); + _subscription = null; + _owner.OnLocalValueBindingCompleted(Property, this); + } + + public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); + public void OnError(Exception error) => OnCompleted(); + + public void OnNext(object? value) + { + if (value is BindingNotification n) + { + value = n.Value; + } + + if (value == AvaloniaProperty.UnsetValue) + { + _owner.ClearLocalValue(Property); + } + else if (value == BindingOperations.DoNothing) + { + // Do nothing! + } + else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) + { + _owner.SetValue(Property, typedValue, BindingPriority.LocalValue); + } + else + { + _owner.ClearLocalValue(Property); + LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value); + } + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/LoggingUtils.cs b/src/Avalonia.Base/PropertyStore/LoggingUtils.cs new file mode 100644 index 0000000000..ecb3a847c1 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LoggingUtils.cs @@ -0,0 +1,61 @@ +using System; +using System.Runtime.CompilerServices; +using Avalonia.Data; +using Avalonia.Logging; + +namespace Avalonia.PropertyStore +{ + internal static class LoggingUtils + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogIfNecessary( + AvaloniaObject owner, + AvaloniaProperty property, + BindingValue value) + { + if (value.HasError) + Log(owner, property, value); + } + + public static void LogInvalidValue( + AvaloniaObject owner, + AvaloniaProperty property, + Type expectedType, + object? value) + { + if (value is not null) + { + owner.GetBindingWarningLogger(property, null)?.Log( + owner, + "Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})", + owner, + property, + expectedType, + value, + value.GetType()); + } + else + { + owner.GetBindingWarningLogger(property, null)?.Log( + owner, + "Error in binding to {Target}.{Property}: expected {ExpectedType}, got null", + owner, + property, + expectedType); + } + } + + private static void Log( + AvaloniaObject owner, + AvaloniaProperty property, + BindingValue value) + { + owner.GetBindingWarningLogger(property, value.Error)?.Log( + owner, + "Error in binding to {Target}.{Property}: {Message}", + owner, + property, + value.Error!.Message); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs deleted file mode 100644 index 182b2638c4..0000000000 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - interface IPriorityValue : IValue - { - void UpdateEffectiveValue(); - } - - /// - /// Stores a set of prioritized values and bindings in a . - /// - /// The property type. - /// - /// When more than a single value or binding is applied to a property in an - /// , the entry in the is converted into - /// a . This class holds any number of - /// entries (sorted first by priority and then in the order - /// they were added) plus a local value. - /// - internal class PriorityValue : IPriorityValue, IValue, IBatchUpdate - { - private readonly AvaloniaObject _owner; - private readonly ValueStore _store; - private readonly List> _entries = new List>(); - private readonly Func? _coerceValue; - private Optional _localValue; - private Optional _value; - private bool _isCalculatingValue; - private bool _batchUpdate; - - public PriorityValue( - AvaloniaObject owner, - StyledPropertyBase property, - ValueStore store) - { - _owner = owner; - Property = property; - _store = store; - - if (property.HasCoercion) - { - var metadata = property.GetMetadata(owner.GetType()); - _coerceValue = metadata.CoerceValue; - } - } - - public PriorityValue( - AvaloniaObject owner, - StyledPropertyBase property, - ValueStore store, - IPriorityValueEntry existing) - : this(owner, property, store) - { - existing.Reparent(this); - _entries.Add(existing); - - if (existing is IBindingEntry binding && - existing.Priority == BindingPriority.LocalValue) - { - // Bit of a special case here: if we have a local value binding that is being - // promoted to a priority value we need to make sure the binding is subscribed - // even if we've got a batch operation in progress because otherwise we don't know - // whether the binding or a subsequent SetValue with local priority will win. A - // notification won't be sent during batch update anyway because it will be - // caught and stored for later by the ValueStore. - binding.Start(ignoreBatchUpdate: true); - } - - var v = existing.GetValue(); - - if (v.HasValue) - { - _value = v; - Priority = existing.Priority; - } - } - - public PriorityValue( - AvaloniaObject owner, - StyledPropertyBase property, - ValueStore sink, - LocalValueEntry existing) - : this(owner, property, sink) - { - _value = _localValue = existing.GetValue(BindingPriority.LocalValue); - Priority = BindingPriority.LocalValue; - } - - public StyledPropertyBase Property { get; } - public BindingPriority Priority { get; private set; } = BindingPriority.Unset; - public IReadOnlyList> Entries => _entries; - Optional IValue.GetValue() => _value.ToObject(); - - public void BeginBatchUpdate() - { - _batchUpdate = true; - - foreach (var entry in _entries) - { - (entry as IBatchUpdate)?.BeginBatchUpdate(); - } - } - - public void EndBatchUpdate() - { - _batchUpdate = false; - - foreach (var entry in _entries) - { - (entry as IBatchUpdate)?.EndBatchUpdate(); - } - - UpdateEffectiveValue(null); - } - - public void ClearLocalValue() - { - _localValue = default; - UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( - _owner, - Property, - default, - default, - BindingPriority.LocalValue)); - } - - public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) - { - if (Priority == BindingPriority.Unset) - { - return default; - } - - if (Priority >= maxPriority) - { - return _value; - } - - return CalculateValue(maxPriority).Item1; - } - - public IDisposable? SetValue(T value, BindingPriority priority) - { - IDisposable? result = null; - - if (priority == BindingPriority.LocalValue) - { - _localValue = value; - } - else - { - var insert = FindInsertPoint(priority); - var entry = new ConstantValueEntry(Property, value, priority, new ValueOwner(this)); - _entries.Insert(insert, entry); - result = entry; - } - - UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( - _owner, - Property, - default, - value, - priority)); - - return result; - } - - public BindingEntry AddBinding(IObservable> source, BindingPriority priority) - { - var binding = new BindingEntry(_owner, Property, source, priority, new(this)); - var insert = FindInsertPoint(binding.Priority); - _entries.Insert(insert, binding); - - if (_batchUpdate) - { - binding.BeginBatchUpdate(); - - if (priority == BindingPriority.LocalValue) - { - binding.Start(ignoreBatchUpdate: true); - } - } - - return binding; - } - - public void UpdateEffectiveValue() => UpdateEffectiveValue(null); - public void Start() => UpdateEffectiveValue(null); - - public void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue) - { - owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - owner, - (AvaloniaProperty)property, - oldValue.Cast(), - newValue.Cast(), - Priority)); - } - - public void ValueChanged(AvaloniaPropertyChangedEventArgs change) - { - if (change.Priority == BindingPriority.LocalValue) - { - _localValue = default; - } - - if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs c) - { - UpdateEffectiveValue(c); - } - } - - public void Completed(IPriorityValueEntry entry, Optional oldValue) - { - _entries.Remove((IPriorityValueEntry)entry); - UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( - _owner, - Property, - oldValue, - default, - entry.Priority)); - } - - private int FindInsertPoint(BindingPriority priority) - { - var result = _entries.Count; - - for (var i = 0; i < _entries.Count; ++i) - { - if (_entries[i].Priority < priority) - { - result = i; - break; - } - } - - return result; - } - - public (Optional, BindingPriority) CalculateValue(BindingPriority maxPriority) - { - _isCalculatingValue = true; - - try - { - for (var i = _entries.Count - 1; i >= 0; --i) - { - var entry = _entries[i]; - - if (entry.Priority < maxPriority) - { - continue; - } - - entry.Start(); - - if (entry.Priority >= BindingPriority.LocalValue && - maxPriority <= BindingPriority.LocalValue && - _localValue.HasValue) - { - return (_localValue, BindingPriority.LocalValue); - } - - var entryValue = entry.GetValue(); - - if (entryValue.HasValue) - { - return (entryValue, entry.Priority); - } - } - - if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue) - { - return (_localValue, BindingPriority.LocalValue); - } - - return (default, BindingPriority.Unset); - } - finally - { - _isCalculatingValue = false; - } - } - - private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs? change) - { - var (value, priority) = CalculateValue(BindingPriority.Animation); - - if (value.HasValue && _coerceValue != null) - { - value = _coerceValue(_owner, value.Value); - } - - Priority = priority; - - if (value != _value) - { - var old = _value; - _value = value; - - _store.ValueChanged(new AvaloniaPropertyChangedEventArgs( - _owner, - Property, - old, - value, - Priority)); - } - else if (change is object) - { - change.MarkNonEffectiveValue(); - change.SetOldValue(default); - _store.ValueChanged(change); - } - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs new file mode 100644 index 0000000000..c95c6444d0 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reactive.Disposables; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + internal class UntypedBindingEntry : IValueEntry, + IObserver, + IDisposable + { + private readonly ValueFrameBase _frame; + private readonly IObservable _source; + private IDisposable? _subscription; + private bool _hasValue; + private T? _value; + + public UntypedBindingEntry( + ValueFrameBase frame, + StyledPropertyBase property, + IObservable source) + { + _frame = frame; + _source = source; + Property = property; + } + + public bool HasValue + { + get + { + StartIfNecessary(); + return _hasValue; + } + } + + public StyledPropertyBase Property { get; } + AvaloniaProperty IValueEntry.Property => Property; + + public void Dispose() + { + Unsubscribe(); + BindingCompleted(); + } + + public T GetValue() + { + StartIfNecessary(); + if (!_hasValue) + throw new AvaloniaInternalException("The binding entry has no value."); + return _value!; + } + + public void Start() + { + Debug.Assert(_subscription is null); + + // Subscription won't be set until Subscribe completes, but in the meantime we + // need to signal that we've started as Subscribe may cause a value to be produced. + _subscription = Disposable.Empty; + _subscription = _source.Subscribe(this); + } + + public bool TryGetValue([MaybeNullWhen(false)] out T value) + { + StartIfNecessary(); + value = _value; + return _hasValue; + } + + public void OnCompleted() => BindingCompleted(); + public void OnError(Exception error) => BindingCompleted(); + + public void OnNext(object? value) => SetValue(value); + + public void OnNext(BindingValue value) + { + if (value.HasValue) + SetValue(value.Value); + else + ClearValue(); + } + + public void Unsubscribe() + { + _subscription?.Dispose(); + _subscription = null; + } + + object? IValueEntry.GetValue() + { + StartIfNecessary(); + if (!_hasValue) + throw new AvaloniaInternalException("The BindingEntry has no value."); + return _value!; + } + + bool IValueEntry.TryGetValue(out object? value) + { + StartIfNecessary(); + value = _value; + return _hasValue; + } + + private void ClearValue() + { + if (_hasValue) + { + _hasValue = false; + _value = default; + _frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); + } + } + + private void SetValue(object? value) + { + if (_frame.Owner is null) + return; + + if (value is BindingNotification n) + { + value = n.Value; + } + + if (value == AvaloniaProperty.UnsetValue) + { + ClearValue(); + } + else if (value == BindingOperations.DoNothing) + { + // Do nothing! + } + else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) + { + if (!_hasValue || !EqualityComparer.Default.Equals(_value, typedValue)) + { + _value = typedValue; + _hasValue = true; + _frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, typedValue); + } + } + else + { + ClearValue(); + LoggingUtils.LogInvalidValue(_frame.Owner.Owner, Property, typeof(T), value); + } + } + + private void BindingCompleted() + { + _subscription = null; + _frame.OnBindingCompleted(this); + } + + private void StartIfNecessary() + { + if (_subscription is null) + Start(); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs b/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs new file mode 100644 index 0000000000..0bdcee3871 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia.Utilities; + +namespace Avalonia.PropertyStore +{ + internal static class UntypedValueUtils + { + public static bool TryConvertAndValidate( + AvaloniaProperty property, + object? value, + out object? result) + { + if (TypeUtilities.TryConvertImplicit(property.PropertyType, value, out result)) + return ((IStyledPropertyAccessor)property).ValidateValue(result); + + result = default; + return false; + } + + public static bool TryConvertAndValidate( + StyledPropertyBase property, + object? value, + [MaybeNullWhen(false)] out T result) + { + if (TypeUtilities.TryConvertImplicit(typeof(T), value, out var v)) + { + result = (T)v!; + + if (property.ValidateValue?.Invoke(result) != false) + return true; + } + + result = default; + return false; + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/ValueFrameBase.cs b/src/Avalonia.Base/PropertyStore/ValueFrameBase.cs new file mode 100644 index 0000000000..09e37e7503 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ValueFrameBase.cs @@ -0,0 +1,54 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; +using Avalonia.Utilities; + +namespace Avalonia.PropertyStore +{ + internal abstract class ValueFrameBase : IValueFrame + { + private readonly AvaloniaPropertyValueStore _entries = new(); + + public int EntryCount => _entries.Count; + public abstract bool IsActive { get; } + public ValueStore? Owner { get; private set; } + public abstract BindingPriority Priority { get; } + + public bool Contains(AvaloniaProperty property) => _entries.Contains(property); + + public IValueEntry GetEntry(int index) => _entries[index]; + + public void SetOwner(ValueStore? owner) => Owner = owner; + + public bool TryGet(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? value) + { + return _entries.TryGetValue(property, out value); + } + + public bool TryGetEntry(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? entry) + { + return _entries.TryGetValue(property, out entry); + } + + public void OnBindingCompleted(IValueEntry binding) + { + Remove(binding.Property); + Owner?.OnBindingCompleted(binding.Property, this); + } + + public virtual void Dispose() + { + for (var i = 0; i < _entries.Count; ++i) + _entries[i].Unsubscribe(); + } + + protected void Add(IValueEntry value) + { + Debug.Assert(!value.Property.IsDirect); + _entries.AddValue(value.Property, value); + } + + protected void Remove(AvaloniaProperty property) => _entries.Remove(property); + protected void Set(IValueEntry value) => _entries.SetValue(value.Property, value); + } +} diff --git a/src/Avalonia.Base/PropertyStore/ValueOwner.cs b/src/Avalonia.Base/PropertyStore/ValueOwner.cs deleted file mode 100644 index c68435f7a5..0000000000 --- a/src/Avalonia.Base/PropertyStore/ValueOwner.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents a union type of and , - /// which are the valid owners of a value store . - /// - /// The value type. - internal readonly struct ValueOwner - { - private readonly ValueStore? _store; - private readonly PriorityValue? _priorityValue; - - public ValueOwner(ValueStore o) - { - _store = o; - _priorityValue = null; - } - - public ValueOwner(PriorityValue v) - { - _store = null; - _priorityValue = v; - } - - public bool IsValueStore => _store is not null; - - public void Completed(StyledPropertyBase property, IPriorityValueEntry entry, Optional oldValue) - { - if (_store is not null) - _store?.Completed(property, entry, oldValue); - else - _priorityValue!.Completed(entry, oldValue); - } - - public void ValueChanged(AvaloniaPropertyChangedEventArgs e) - { - if (_store is not null) - _store?.ValueChanged(e); - else - _priorityValue!.ValueChanged(e); - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs new file mode 100644 index 0000000000..093235955e --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -0,0 +1,948 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Avalonia.Collections.Pooled; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Logging; + +namespace Avalonia.PropertyStore +{ + internal class ValueStore + { + private readonly List _frames = new(); + private Dictionary? _localValueBindings; + private InheritanceFrame? _inheritanceFrame; + private Dictionary? _effectiveValues; + private int _frameGeneration; + private int _styling; + + public ValueStore(AvaloniaObject owner) => Owner = owner; + + public AvaloniaObject Owner { get; } + public IReadOnlyList Frames => _frames; + + public void BeginStyling() => ++_styling; + + public void EndStyling() + { + if (--_styling == 0) + ReevaluateEffectiveValues(); + } + + public void AddFrame(IValueFrame style) + { + InsertFrame(style); + ReevaluateEffectiveValues(); + } + + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + var observer = new LocalValueBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + else + { + var effective = GetEffectiveValue(property); + var frame = GetOrCreateImmediateValueFrame(property, priority); + var result = frame.AddBinding(property, source); + + if (effective is null || priority <= effective.Priority) + result.Start(); + + return result; + } + } + + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable source, + BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + var observer = new LocalValueBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + else + { + var effective = GetEffectiveValue(property); + var frame = GetOrCreateImmediateValueFrame(property, priority); + var result = frame.AddBinding(property, source); + + if (effective is null || priority <= effective.Priority) + result.Start(); + + return result; + } + } + + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable source, + BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + var observer = new LocalValueUntypedBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + else + { + var effective = GetEffectiveValue(property); + var frame = GetOrCreateImmediateValueFrame(property, priority); + var result = frame.AddBinding(property, source); + + if (effective is null || priority <= effective.Priority) + result.Start(); + + return result; + } + } + + public void ClearLocalValue(AvaloniaProperty property) + { + if (TryGetEffectiveValue(property, out var effective) && + effective.Priority == BindingPriority.LocalValue) + { + ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true); + } + } + + public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) + { + if (property.ValidateValue?.Invoke(value) == false) + { + throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); + } + + IDisposable? result = null; + + if (priority != BindingPriority.LocalValue) + { + var frame = GetOrCreateImmediateValueFrame(property, priority); + result = frame.AddValue(property, value); + InsertFrame(frame); + } + + if (TryGetEffectiveValue(property, out var existing)) + { + var effective = (EffectiveValue)existing; + effective.SetAndRaise(this, property, value, priority); + } + else + { + AddEffectiveValueAndRaise(property, value, priority); + } + + return result; + } + + public object? GetValue(AvaloniaProperty property) + { + if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v)) + return v.Value; + if (_inheritanceFrame is not null && _inheritanceFrame.TryGetFromThisOrAncestor(property, out v)) + return v.Value; + + return GetDefaultValue(property); + } + + public T GetValue(StyledPropertyBase property) + { + if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v)) + return ((EffectiveValue)v).Value; + if (_inheritanceFrame is not null && _inheritanceFrame.TryGetFromThisOrAncestor(property, out v)) + return ((EffectiveValue)v).Value; + return property.GetDefaultValue(Owner.GetType()); + } + + public bool IsAnimating(AvaloniaProperty property) + { + if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v)) + return v.Priority <= BindingPriority.Animation; + return false; + } + + public bool IsSet(AvaloniaProperty property) + { + if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v)) + return v.Priority < BindingPriority.Inherited; + return false; + } + + public Optional GetBaseValue(StyledPropertyBase property) + { + if (TryGetEffectiveValue(property, out var v) && + ((EffectiveValue)v).TryGetBaseValue(out var baseValue)) + { + return baseValue; + } + + return default; + } + + public void SetInheritanceParent(AvaloniaObject? oldParent, AvaloniaObject? newParent) + { + var values = DictionaryPool.Get(); + var oldInheritanceFrame = oldParent?.GetValueStore()._inheritanceFrame; + var newInheritanceFrame = newParent?.GetValueStore().OnBecameInheritanceParent(); + + // The old and new parents are the same, nothing to do here. + if (oldInheritanceFrame == newInheritanceFrame) + return; + + // First get the old values from the old inheritance parent. + var f = oldInheritanceFrame; + + while (f is not null) + { + foreach (var i in f) + { + values.TryAdd(i.Key, new(i.Value)); + } + f = f.Parent; + } + + f = newInheritanceFrame; + + // Get the new values from the new inheritance parent. + while (f is not null) + { + foreach (var i in f) + { + if (values.TryGetValue(i.Key, out var existing)) + values[i.Key] = existing.WithNewValue(i.Value); + else + values.Add(i.Key, new(null, i.Value)); + } + f = f.Parent; + } + + ParentInheritanceFrameChanged(newInheritanceFrame); + + // Raise PropertyChanged events where necessary on this object and inheritance children. + foreach (var i in values) + { + var oldValue = i.Value.OldValue; + var newValue = i.Value.NewValue; + + if (oldValue != newValue) + InheritedValueChanged(i.Key, oldValue, newValue); + } + + DictionaryPool.Release(values); + } + + public void FrameActivationChanged(IValueFrame frame) + { + ReevaluateEffectiveValues(); + } + + /// + /// Called by an inheritance child to notify the value store that it has become an + /// inheritance parent. Creates and returns an inheritance frame if necessary. + /// + /// + public InheritanceFrame? OnBecameInheritanceParent() + { + if (_inheritanceFrame is not null) + return _inheritanceFrame; + if (_effectiveValues is null) + return null; + + foreach (var i in _effectiveValues) + { + if (i.Key.Inherits) + return GetOrCreateInheritanceFrame(true); + } + + return null; + } + + /// + /// Called by non-LocalValue binding entries to re-evaluate the effective value when the + /// binding produces a new value. + /// + /// The bound property. + /// The priority of binding which produced a new value. + /// The new value. + public void OnBindingValueChanged( + AvaloniaProperty property, + BindingPriority priority, + object? value) + { + Debug.Assert(priority != BindingPriority.LocalValue); + + if (TryGetEffectiveValue(property, out var existing)) + { + if (priority <= existing.Priority) + ReevaluateEffectiveValue(property, existing); + } + else + { + AddEffectiveValueAndRaise(property, value, priority); + } + } + + /// + /// Called by non-LocalValue binding entries to re-evaluate the effective value when the + /// binding produces a new value. + /// + /// The bound property. + /// The priority of binding which produced a new value. + /// The new value. + public void OnBindingValueChanged( + StyledPropertyBase property, + BindingPriority priority, + T value) + { + Debug.Assert(priority != BindingPriority.LocalValue); + + if (TryGetEffectiveValue(property, out var existing)) + { + if (priority <= existing.Priority) + ReevaluateEffectiveValue(property, existing); + } + else + { + AddEffectiveValueAndRaise(property, value, priority); + } + } + + /// + /// Called by non-LocalValue binding entries to re-evaluate the effective value when the + /// binding produces an unset value. + /// + /// The bound property. + /// The priority of binding which produced a new value. + public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority) + { + Debug.Assert(priority != BindingPriority.LocalValue); + + if (TryGetEffectiveValue(property, out var existing)) + { + if (priority <= existing.Priority) + ReevaluateEffectiveValue(property, existing); + } + } + + /// + /// Called by a to re-evaluate the effective value when the + /// binding completes or terminates on error. + /// + /// The previously bound property. + /// The frame which contained the binding. + public void OnBindingCompleted(AvaloniaProperty property, IValueFrame frame) + { + var priority = frame.Priority; + + if (TryGetEffectiveValue(property, out var existing)) + { + if (priority <= existing.Priority) + ReevaluateEffectiveValue(property, existing); + } + } + + /// + /// Called by when an property with inheritance enabled + /// changes its value on this value store. + /// + /// The property whose value changed. + /// The old value of the property. + /// The effective value instance. + public void OnInheritedEffectiveValueChanged( + StyledPropertyBase property, + T oldValue, + EffectiveValue value) + { + Debug.Assert(property.Inherits); + + var children = Owner.GetInheritanceChildren(); + + // If we have children or an existing inheritance frame, then make sure it's owned and + // set the value. If we have no children and no inheritance frame then it will be + // created when it's needed. + if (children is not null || _inheritanceFrame is not null) + GetOrCreateInheritanceFrame(true)[property] = value; + + if (children is not null) + { + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().OnParentInheritedValueChanged(property, oldValue, value.Value); + } + } + } + + /// + /// Called by when an property with inheritance enabled + /// is removed from the effective values. + /// + /// The property whose value changed. + /// The old value of the property. + public void OnInheritedEffectiveValueDisposed(StyledPropertyBase property, T oldValue) + { + Debug.Assert(property.Inherits); + Debug.Assert(_inheritanceFrame is null || _inheritanceFrame.Owner == this); + + if (_inheritanceFrame is null || _inheritanceFrame.Owner != this) + return; + + _inheritanceFrame.Remove(property); + + var children = Owner.GetInheritanceChildren(); + + if (children is not null) + { + var defaultValue = property.GetDefaultValue(Owner.GetType()); + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().OnParentInheritedValueChanged(property, oldValue, defaultValue); + } + } + } + + /// + /// Called when a completes. + /// + /// The previously bound property. + /// The observer. + public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer) + { + if (_localValueBindings is not null && + _localValueBindings.TryGetValue(property.Id, out var existing)) + { + if (existing == observer) + { + _localValueBindings?.Remove(property.Id); + ClearLocalValue(property); + } + } + } + + public void OnParentInheritedValueChanged( + StyledPropertyBase property, + T oldValue, + T newValue) + { + Debug.Assert(property.Inherits); + + // Ensure the inheritance frame is created. + GetOrCreateInheritanceFrame(false); + + // If the inherited value is set locally, propagation stops here. + if (_effectiveValues is not null && _effectiveValues.ContainsKey(property)) + return; + + Owner.RaisePropertyChanged( + property, + oldValue, + newValue, + BindingPriority.Inherited, + true); + + var children = Owner.GetInheritanceChildren(); + + if (children is null) + return; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().OnParentInheritedValueChanged(property, oldValue, newValue); + } + } + + /// + /// Called by an to re-evaluate the effective value when a value + /// is removed. + /// + /// The frame on which the change occurred. + /// The property whose value was removed. + public void OnValueEntryRemoved(IValueFrame frame, AvaloniaProperty property) + { + Debug.Assert(frame.IsActive); + + if (TryGetEffectiveValue(property, out var existing)) + { + if (frame.Priority <= existing.Priority) + ReevaluateEffectiveValue(property, existing); + } + else + { + Logger.TryGet(LogEventLevel.Error, LogArea.Property)?.Log( + Owner, + "Internal error: ValueStore.OnEntryRemoved called for {Property} " + + "but no effective value was found.", + property); + Debug.Assert(false); + } + } + + public bool RemoveFrame(IValueFrame frame) + { + if (_frames.Remove(frame)) + { + frame.Dispose(); + ++_frameGeneration; + ReevaluateEffectiveValues(); + } + + return false; + } + + public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property) + { + var effective = GetEffectiveValue(property); + return new AvaloniaPropertyValue( + property, + effective?.Value, + effective?.Priority ?? BindingPriority.Unset, + null); + } + + private void InsertFrame(IValueFrame frame) + { + var index = _frames.BinarySearch(frame, FrameInsertionComparer.Instance); + if (index < 0) + index = ~index; + _frames.Insert(index, frame); + ++_frameGeneration; + frame.SetOwner(this); + } + + private InheritanceFrame GetOrCreateInheritanceFrame(bool owned) + { + if (_inheritanceFrame is null) + { + var parentFrame = Owner.InheritanceParent?.GetValueStore()._inheritanceFrame; + + _inheritanceFrame = owned || parentFrame is null ? + new(this, parentFrame) : + parentFrame; + + if (_effectiveValues is not null) + { + foreach (var i in _effectiveValues) + { + if (i.Key.Inherits) + _inheritanceFrame[i.Key] = i.Value; + } + } + } + else if (owned && _inheritanceFrame.Owner != this) + { + _inheritanceFrame = new(this, _inheritanceFrame); + } + + return _inheritanceFrame; + } + + private ImmediateValueFrame GetOrCreateImmediateValueFrame( + AvaloniaProperty property, + BindingPriority priority) + { + Debug.Assert(priority != BindingPriority.LocalValue); + + // TODO: Binary search? + for (var i = _frames.Count - 1; i >= 0; --i) + { + var frame = _frames[i]; + if (frame is ImmediateValueFrame immediate && !immediate.Contains(property)) + return immediate; + if (frame.Priority > priority) + break; + } + + var result = new ImmediateValueFrame(priority); + InsertFrame(result); + return result; + } + + private void ReevaluateEffectiveValue( + AvaloniaProperty property, + EffectiveValue current, + bool ignoreLocalValue = false) + { + if (EvaluateEffectiveValue( + property, + !ignoreLocalValue ? current : null, + out var value, + out var priority, + out var baseValue, + out var basePriority)) + { + if (basePriority != BindingPriority.Unset) + current.SetAndRaise(this, property, value, priority, baseValue, basePriority); + else + current.SetAndRaise(this, property, value, priority); + } + else + { + _effectiveValues?.Remove(property); + current.DisposeAndRaiseUnset(this, property); + } + } + + /// + /// Adds a new effective value, raises the initial + /// event and notifies inheritance children if necessary . + /// + /// The property type. + /// The property. + /// The property value. + /// The value priority. + private void AddEffectiveValueAndRaise(AvaloniaProperty property, object? value, BindingPriority priority) + { + Debug.Assert(priority < BindingPriority.Inherited); + var effectiveValue = property.CreateEffectiveValue(Owner); + _effectiveValues ??= new(); + _effectiveValues.Add(property, effectiveValue); + effectiveValue.SetAndRaise(this, property, value, priority); + } + + /// + /// Adds a new effective value, raises the initial + /// event and notifies inheritance children if necessary . + /// + /// The property type. + /// The property. + /// The property value. + /// The value priority. + private void AddEffectiveValueAndRaise(StyledPropertyBase property, T value, BindingPriority priority) + { + Debug.Assert(priority < BindingPriority.Inherited); + var defaultValue = property.GetDefaultValue(Owner.GetType()); + var effectiveValue = new EffectiveValue(defaultValue, BindingPriority.Unset); + _effectiveValues ??= new(); + _effectiveValues.Add(property, effectiveValue); + effectiveValue.SetAndRaise(this, property, value, priority); + } + + /// + /// Evaluates the current value and base value for a property based on the current frames and optionally + /// local values. Does not evaluate inherited values. + /// + /// The property to evaluation + /// The current effective value if the local value is to be considered. + /// When the method exits will contain the current value if it exists. + /// When the method exits will contain the current value priority. + /// >When the method exits will contain the current base value if it exists. + /// When the method exits will contain the current base value priority. + /// + /// True if a value was found, otherwise false. + /// + private bool EvaluateEffectiveValue( + AvaloniaProperty property, + EffectiveValue? current, + out object? value, + out BindingPriority priority, + out object? baseValue, + out BindingPriority basePriority) + { + var i = _frames.Count - 1; + + value = baseValue = AvaloniaProperty.UnsetValue; + priority = basePriority = BindingPriority.Unset; + + // First try to find an animation value. + for (; i >= 0; --i) + { + var frame = _frames[i]; + + if (frame.Priority > BindingPriority.Animation) + break; + + if (frame.IsActive && + frame.TryGetEntry(property, out var entry) && + entry.TryGetValue(out value)) + { + priority = frame.Priority; + --i; + break; + } + } + + // Local values come from the current EffectiveValue. + if (current?.Priority == BindingPriority.LocalValue) + { + // If there's a current effective local value and no animated value then we use the + // effective local value. + if (priority == BindingPriority.Unset) + { + value = current.Value; + priority = BindingPriority.LocalValue; + } + + // The local value is always the base value. + baseValue = current.Value; + basePriority = BindingPriority.LocalValue; + return true; + } + + // Or the current effective base value if there's no longer an animated value. + if (priority == BindingPriority.Unset && current?.BasePriority == BindingPriority.LocalValue) + { + value = baseValue = current.BaseValue; + priority = basePriority = BindingPriority.LocalValue; + return true; + } + + // Now try the rest of the frames. + for (; i >= 0; --i) + { + var frame = _frames[i]; + + if (frame.IsActive && + frame.TryGetEntry(property, out var entry) && + entry.TryGetValue(out var v)) + { + if (priority == BindingPriority.Unset) + { + value = v; + priority = frame.Priority; + } + + baseValue = v; + basePriority = frame.Priority; + return true; + } + } + + return priority != BindingPriority.Unset; + } + + private void InheritedValueChanged( + AvaloniaProperty property, + EffectiveValue? oldValue, + EffectiveValue? newValue) + { + Debug.Assert(oldValue != newValue); + Debug.Assert(oldValue is not null || newValue is not null); + + // If the value is set locally, propagaton ends here. + if (_effectiveValues?.ContainsKey(property) == true) + return; + + // Raise PropertyChanged on this object if necessary. + (oldValue ?? newValue!).RaiseInheritedValueChanged(Owner, property, oldValue, newValue); + + var children = Owner.GetInheritanceChildren(); + + if (children is null) + return; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().InheritedValueChanged(property, oldValue, newValue); + } + } + + private void ParentInheritanceFrameChanged(InheritanceFrame? parent) + { + if (_inheritanceFrame?.Owner == this) + { + _inheritanceFrame.SetParent(parent); + } + else if (_inheritanceFrame != parent) + { + _inheritanceFrame = parent; + + var children = Owner.GetInheritanceChildren(); + + if (children is null) + return; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().ParentInheritanceFrameChanged(parent); + } + } + } + + private void ReevaluateEffectiveValues() + { + restart: + // Don't reevaluate if a styling pass is in effect, reevaluation will be done when + // it has finished. + if (_styling > 0) + return; + + var generation = _frameGeneration; + + // Reset all non-LocalValue effective values to Unset priority. + if (_effectiveValues is not null) + { + foreach (var v in _effectiveValues) + { + var e = v.Value; + + if (e.Priority != BindingPriority.LocalValue) + e.SetPriority(BindingPriority.Unset); + if (e.BasePriority != BindingPriority.LocalValue) + e.SetBasePriority(BindingPriority.Unset); + } + } + + // Iterate the frames, setting and creating effective values. + for (var i = _frames.Count - 1; i >= 0; --i) + { + var frame = _frames[i]; + + if (!frame.IsActive) + continue; + + var priority = frame.Priority; + var count = frame.EntryCount; + + for (var j = 0; j < count; ++j) + { + var entry = frame.GetEntry(j); + + if (!entry.HasValue) + continue; + + var property = entry.Property; + + if (_effectiveValues is not null && + _effectiveValues.TryGetValue(property, out var effectiveValue)) + { + if (effectiveValue.Priority == BindingPriority.Unset || + effectiveValue.BasePriority == BindingPriority.Unset) + { + effectiveValue.SetAndRaise(this, entry, priority); + } + } + else + { + var v = property.CreateEffectiveValue(Owner); + _effectiveValues ??= new(); + _effectiveValues.Add(property, v); + v.SetAndRaise(this, entry, priority); + } + + if (generation != _frameGeneration) + goto restart; + } + } + + // Remove all effective values that are still unset. + if (_effectiveValues is not null) + { + PooledList? remove = null; + + foreach (var v in _effectiveValues) + { + var e = v.Value; + + if (e.Priority == BindingPriority.Unset) + { + remove ??= new(); + remove.Add(v.Key); + } + } + + if (remove is not null) + { + foreach (var v in remove) + { + if (_effectiveValues.Remove(v, out var e)) + e.DisposeAndRaiseUnset(this, v); + } + remove.Dispose(); + } + } + } + + [MemberNotNullWhen(true, nameof(_effectiveValues))] + private bool TryGetEffectiveValue( + AvaloniaProperty property, + [NotNullWhen(true)] out EffectiveValue? value) + { + if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out value)) + return true; + value = null; + return false; + } + + private EffectiveValue? GetEffectiveValue(AvaloniaProperty property) + { + if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var value)) + return value; + return null; + } + + private object? GetDefaultValue(AvaloniaProperty property) + { + return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType()); + } + + private void DisposeExistingLocalValueBinding(AvaloniaProperty property) + { + if (_localValueBindings is not null && + _localValueBindings.TryGetValue(property.Id, out var existing)) + { + existing.Dispose(); + } + } + + private class FrameInsertionComparer : IComparer + { + public static readonly FrameInsertionComparer Instance = new FrameInsertionComparer(); + public int Compare(IValueFrame? x, IValueFrame? y) + { + var result = y!.Priority - x!.Priority; + return result != 0 ? result : -1; + } + } + + private readonly struct OldNewValue + { + public OldNewValue(EffectiveValue? oldValue) + { + OldValue = oldValue; + NewValue = null; + } + + public OldNewValue(EffectiveValue? oldValue, EffectiveValue? newValue) + { + OldValue = oldValue; + NewValue = newValue; + } + + public readonly EffectiveValue? OldValue; + public readonly EffectiveValue? NewValue; + + public OldNewValue WithNewValue(EffectiveValue newValue) => new(OldValue, newValue); + } + } +} diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index ecf5d95ffc..bd012f74d6 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Linq; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Controls; @@ -69,7 +70,6 @@ namespace Avalonia private IResourceDictionary? _resources; private Styles? _styles; private bool _styled; - private List? _appliedStyles; private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; private bool _hasPromotedTheme; @@ -351,15 +351,21 @@ namespace Avalonia { if (_initCount == 0 && !_styled) { - try - { - BeginBatchUpdate(); - AvaloniaLocator.Current.GetService()?.ApplyStyles(this); - } - finally + var styler = AvaloniaLocator.Current.GetService(); + + if (styler is object) { - _styled = true; - EndBatchUpdate(); + GetValueStore().BeginStyling(); + + try + { + styler.ApplyStyles(this); + } + finally + { + _styled = true; + GetValueStore().EndStyling(); + } } if (_hasPromotedTheme) @@ -389,14 +395,15 @@ namespace Avalonia internal StyleDiagnostics GetStyleDiagnosticsInternal() { - IReadOnlyList? appliedStyles = _appliedStyles; + var styles = new List(); - if (appliedStyles is null) + foreach (var frame in GetValueStore().Frames) { - appliedStyles = Array.Empty(); + if (frame is IStyleInstance style) + styles.Add(style); } - return new StyleDiagnostics(appliedStyles); + return new StyleDiagnostics(styles); } /// @@ -522,20 +529,8 @@ namespace Avalonia return null; } - void IStyleable.StyleApplied(IStyleInstance instance) - { - instance = instance ?? throw new ArgumentNullException(nameof(instance)); - - _appliedStyles ??= new List(); - _appliedStyles.Add(instance); - } - void IStyleable.DetachStyles() => DetachStyles(); - void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); - - void IStyleable.InvalidateStyles() => InvalidateStyles(); - void IStyleHost.StylesAdded(IReadOnlyList styles) { InvalidateStylesOnThisAndDescendents(); @@ -830,56 +825,25 @@ namespace Avalonia } } - private void DetachStyles() + private void DetachStyles(IReadOnlyList? styles = null) { - if (_appliedStyles?.Count > 0) - { - BeginBatchUpdate(); + var valueStore = GetValueStore(); - try - { - foreach (var i in _appliedStyles) - { - i.Dispose(); - } + valueStore.BeginStyling(); - _appliedStyles.Clear(); - } - finally + for (var i = valueStore.Frames.Count - 1; i >= 0; --i) + { + if (valueStore.Frames[i] is StyleInstance si && + (styles is null || styles.Contains(si.Source))) { - EndBatchUpdate(); + valueStore.RemoveFrame(si); } } + valueStore.EndStyling(); _styled = false; } - private void DetachStyles(IReadOnlyList styles) - { - styles = styles ?? throw new ArgumentNullException(nameof(styles)); - - if (_appliedStyles is null) - { - return; - } - - var count = styles.Count; - - for (var i = 0; i < count; ++i) - { - for (var j = _appliedStyles.Count - 1; j >= 0; --j) - { - var applied = _appliedStyles[j]; - - if (applied.Source == styles[i]) - { - applied.Dispose(); - _appliedStyles.RemoveAt(j); - } - } - } - } - private void InvalidateStylesOnThisAndDescendents() { InvalidateStyles(); @@ -895,7 +859,7 @@ namespace Avalonia } } - private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) + private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) { DetachStyles(styles); @@ -927,38 +891,24 @@ namespace Avalonia } } - private static IReadOnlyList RecurseStyles(IReadOnlyList styles) + private static IReadOnlyList RecurseStyles(IReadOnlyList styles) { - var count = styles.Count; - List? result = null; - - for (var i = 0; i < count; ++i) - { - var style = styles[i]; - - if (style.Children.Count > 0) - { - if (result is null) - { - result = new List(styles); - } - - RecurseStyles(style.Children, result); - } - } - - return result ?? styles; + var result = new List(); + RecurseStyles(styles, result); + return result; } - private static void RecurseStyles(IReadOnlyList styles, List result) + private static void RecurseStyles(IReadOnlyList styles, List result) { var count = styles.Count; for (var i = 0; i < count; ++i) { - var style = styles[i]; - result.Add(style); - RecurseStyles(style.Children, result); + var s = styles[i]; + if (s is StyleBase style) + result.Add(style); + else if (s is IReadOnlyList children) + RecurseStyles(children, result); } } } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index da607720ff..eed9df71e0 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -1,7 +1,10 @@ using System; +using System.Reflection; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Reactive; using Avalonia.Styling; +using Avalonia.Utilities; namespace Avalonia { @@ -169,6 +172,20 @@ namespace Avalonia /// object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + bool IStyledPropertyAccessor.ValidateValue(object? value) + { + if (value is null && !typeof(TValue).IsValueType) + return ValidateValue?.Invoke(default!) ?? true; + if (value is TValue typed) + return ValidateValue?.Invoke(typed) ?? true; + return false; + } + + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) + { + return new EffectiveValue(GetDefaultValue(o.GetType()), BindingPriority.Unset); + } + /// internal override void RouteClearValue(AvaloniaObject o) { @@ -182,34 +199,44 @@ namespace Avalonia } /// - internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(AvaloniaObject o) { - var value = o.GetBaseValue(this, maxPriority); + var value = o.GetBaseValue(this); return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; } /// internal override IDisposable? RouteSetValue( - AvaloniaObject o, + AvaloniaObject target, object? value, BindingPriority priority) { - var v = TryConvert(value); - - if (v.HasValue) + if (value == BindingOperations.DoNothing) { - return o.SetValue(this, (TValue)v.Value!, priority); + return null; } - else if (v.Type == BindingValueType.UnsetValue) + else if (value == UnsetValue) { - o.ClearValue(this); + target.ClearValue(this); + return null; } - else if (v.HasError) + else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) { - throw v.Error!; + return target.SetValue(this, (TValue)converted!, priority); } + else + { + var type = value?.GetType().FullName ?? "(null)"; + throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); + } + } - return null; + internal override IDisposable RouteBind( + AvaloniaObject target, + IObservable source, + BindingPriority priority) + { + return target.Bind(this, source, priority); } /// @@ -222,39 +249,6 @@ namespace Avalonia return o.Bind(this, adapter, priority); } - /// - internal override void RouteInheritanceParentChanged( - AvaloniaObject o, - AvaloniaObject? oldParent) - { - o.InheritanceParentChanged(this, oldParent); - } - - internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value) - { - if (value is IBinding binding) - { - return new PropertySetterBindingInstance( - target, - this, - binding); - } - else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType)) - { - return new PropertySetterTemplateInstance( - target, - this, - template); - } - else - { - return new PropertySetterInstance( - target, - this, - (TValue)value!); - } - } - private object? GetDefaultBoxedValue(Type type) { _ = type ?? throw new ArgumentNullException(nameof(type)); diff --git a/src/Avalonia.Base/Styling/Activators/AndActivator.cs b/src/Avalonia.Base/Styling/Activators/AndActivator.cs index 0e1e3b565b..953d0a4953 100644 --- a/src/Avalonia.Base/Styling/Activators/AndActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/AndActivator.cs @@ -16,6 +16,23 @@ namespace Avalonia.Styling.Activators public int Count => _sources?.Count ?? 0; + public override bool IsActive + { + get + { + if (_sources is null) + return false; + + foreach (var source in _sources) + { + if (!source.IsActive) + return false; + } + + return true; + } + } + public void Add(IStyleActivator activator) { _sources ??= new List(); diff --git a/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs b/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs index ac7b8b3ef1..3dee6aaab6 100644 --- a/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs @@ -18,6 +18,11 @@ namespace Avalonia.Styling.Activators [Unstable] public interface IStyleActivator : IDisposable { + /// + /// Gets a value indicating whether the style is activated. + /// + bool IsActive { get; } + /// /// Subscribes to the activator. /// diff --git a/src/Avalonia.Base/Styling/Activators/NotActivator.cs b/src/Avalonia.Base/Styling/Activators/NotActivator.cs index 1bb6ed3cd2..1735b265d1 100644 --- a/src/Avalonia.Base/Styling/Activators/NotActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NotActivator.cs @@ -9,6 +9,7 @@ namespace Avalonia.Styling.Activators { private readonly IStyleActivator _source; public NotActivator(IStyleActivator source) => _source = source; + public override bool IsActive => !_source.IsActive; void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); protected override void Initialize() => _source.Subscribe(this, 0); protected override void Deinitialize() => _source.Unsubscribe(this); diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs index 6f54cd5904..87f181b884 100644 --- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs @@ -26,9 +26,11 @@ namespace Avalonia.Styling.Activators _reversed = reversed; } + public override bool IsActive => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; + protected override void Initialize() { - PublishNext(IsMatching()); + PublishNext(IsActive); _provider.ChildIndexChanged += ChildIndexChanged; } @@ -47,10 +49,8 @@ namespace Avalonia.Styling.Activators || e.Child is null || e.Child == _control) { - PublishNext(IsMatching()); + PublishNext(IsActive); } } - - private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; } } diff --git a/src/Avalonia.Base/Styling/Activators/OrActivator.cs b/src/Avalonia.Base/Styling/Activators/OrActivator.cs index fcb7d71e60..7b206a1d34 100644 --- a/src/Avalonia.Base/Styling/Activators/OrActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/OrActivator.cs @@ -16,6 +16,23 @@ namespace Avalonia.Styling.Activators public int Count => _sources?.Count ?? 0; + public override bool IsActive + { + get + { + if (_sources is null) + return false; + + foreach (var source in _sources) + { + if (source.IsActive) + return true; + } + + return false; + } + } + public void Add(IStyleActivator activator) { _sources ??= new List(); diff --git a/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs index 69de665485..d98959d40b 100644 --- a/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs @@ -24,6 +24,15 @@ namespace Avalonia.Styling.Activators _value = value; } + public override bool IsActive + { + get + { + var value = _control.GetValue(_property); + return PropertyEqualsSelector.Compare(_property.PropertyType, value, _value); + } + } + protected override void Initialize() { _subscription = _control.GetObservable(_property).Subscribe(this); @@ -33,6 +42,6 @@ namespace Avalonia.Styling.Activators void IObserver.OnCompleted() { } void IObserver.OnError(Exception error) { } - void IObserver.OnNext(object? value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value)); + void IObserver.OnNext(object? value) => PublishNext(IsActive); } } diff --git a/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs b/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs index 578098b2b0..c9645dcbc2 100644 --- a/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs +++ b/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Avalonia.Styling.Activators { /// @@ -11,6 +9,8 @@ namespace Avalonia.Styling.Activators private int _tag; private bool? _value; + public abstract bool IsActive { get; } + public void Subscribe(IStyleActivatorSink sink, int tag = 0) { if (_sink is null) diff --git a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs index 3f70ff50b3..de27e797ea 100644 --- a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs @@ -22,6 +22,8 @@ namespace Avalonia.Styling.Activators _match = match; } + public override bool IsActive => AreClassesMatching(_classes, _match); + public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) { int remainingMatches = toMatch.Count; @@ -54,12 +56,12 @@ namespace Avalonia.Styling.Activators void IClassesChangedListener.Changed() { - PublishNext(IsMatching()); + PublishNext(IsActive); } protected override void Initialize() { - PublishNext(IsMatching()); + PublishNext(IsActive); _classes.AddListener(this); } @@ -67,7 +69,5 @@ namespace Avalonia.Styling.Activators { _classes.RemoveListener(this); } - - private bool IsMatching() => AreClassesMatching(_classes, _match); } } diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 644e8b32d4..8fd94297b6 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.PropertyStore; namespace Avalonia.Styling { diff --git a/src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs b/src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs new file mode 100644 index 0000000000..a95835b7fc --- /dev/null +++ b/src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Styling +{ + internal class DirectPropertySetterBindingInstance : ISetterInstance + { + } +} diff --git a/src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs b/src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs new file mode 100644 index 0000000000..1707be454f --- /dev/null +++ b/src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Styling +{ + internal class DirectPropertySetterInstance : ISetterInstance + { + } +} diff --git a/src/Avalonia.Base/Styling/ISetter.cs b/src/Avalonia.Base/Styling/ISetter.cs index 71ae5d84c0..87ba6c5680 100644 --- a/src/Avalonia.Base/Styling/ISetter.cs +++ b/src/Avalonia.Base/Styling/ISetter.cs @@ -12,13 +12,13 @@ namespace Avalonia.Styling /// /// Instances a setter on a control. /// + /// The style which contains the setter. /// The control. /// An . /// /// This method should return an which can be used to apply - /// the setter to the specified control. Note that it should not apply the setter value - /// until is called. + /// the setter to the specified control. /// - ISetterInstance Instance(IStyleable target); + ISetterInstance Instance(IStyleInstance styleInstance, IStyleable target); } } diff --git a/src/Avalonia.Base/Styling/ISetterInstance.cs b/src/Avalonia.Base/Styling/ISetterInstance.cs index e0d3137619..4a65d6deeb 100644 --- a/src/Avalonia.Base/Styling/ISetterInstance.cs +++ b/src/Avalonia.Base/Styling/ISetterInstance.cs @@ -1,40 +1,12 @@ -using System; -using Avalonia.Metadata; +using Avalonia.Metadata; namespace Avalonia.Styling { /// - /// Represents a setter that has been instanced on a control. + /// Represents an that has been instanced on a control. /// [Unstable] - public interface ISetterInstance : IDisposable + public interface ISetterInstance { - /// - /// Starts the setter instance. - /// - /// Whether the parent style has an activator. - /// - /// If is false then the setter should be immediately - /// applied and and should not be called. - /// If true, then bindings etc should be initiated but not produce a value until - /// called. - /// - public void Start(bool hasActivator); - - /// - /// Activates the setter. - /// - /// - /// Should only be called if hasActivator was true when was called. - /// - public void Activate(); - - /// - /// Deactivates the setter. - /// - /// - /// Should only be called if hasActivator was true when was called. - /// - public void Deactivate(); } } diff --git a/src/Avalonia.Base/Styling/IStyleInstance.cs b/src/Avalonia.Base/Styling/IStyleInstance.cs index 262f336e05..749a2c84d5 100644 --- a/src/Avalonia.Base/Styling/IStyleInstance.cs +++ b/src/Avalonia.Base/Styling/IStyleInstance.cs @@ -1,13 +1,12 @@ -using System; -using Avalonia.Metadata; +using Avalonia.Metadata; namespace Avalonia.Styling { /// - /// Represents a style that has been instanced on a control. + /// Represents a that has been instanced on a control. /// [Unstable] - public interface IStyleInstance : IDisposable + public interface IStyleInstance { /// /// Gets the source style. @@ -15,18 +14,16 @@ namespace Avalonia.Styling IStyle Source { get; } /// - /// Gets a value indicating whether this style has an activator. + /// Gets a value indicating whether this style instance has an activator. /// + /// + /// A style instance without an activator will always be active. + /// bool HasActivator { get; } - + /// /// Gets a value indicating whether this style is active. /// bool IsActive { get; } - - /// - /// Instructs the style to start acting upon the control. - /// - void Start(); } } diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index 254da4d85c..e94fc5c4e6 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/src/Avalonia.Base/Styling/IStyleable.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia.Collections; using Avalonia.Metadata; @@ -31,25 +30,6 @@ namespace Avalonia.Styling /// ControlTheme? GetEffectiveTheme(); - /// - /// Notifies the element that a style has been applied. - /// - /// The style instance. - void StyleApplied(IStyleInstance instance); - - /// - /// Detaches all styles applied to the element. - /// void DetachStyles(); - - /// - /// Detaches a collection of styles, if applied to the element. - /// - void DetachStyles(IReadOnlyList styles); - - /// - /// Detaches all styles from the element and queues a restyle. - /// - void InvalidateStyles(); } } diff --git a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs index edd3fb7d48..18d23ab70b 100644 --- a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs @@ -1,200 +1,41 @@ using System; -using System.Reactive.Subjects; +using System.Reactive.Linq; using Avalonia.Data; -using Avalonia.Reactive; - -#nullable enable +using Avalonia.PropertyStore; namespace Avalonia.Styling { - /// - /// A which has been instanced on a control and has an - /// as its value. - /// - /// The target property type. - internal class PropertySetterBindingInstance : SingleSubscriberObservableBase>, - ISubject>, - ISetterInstance + internal class PropertySetterBindingInstance : BindingEntry, ISetterInstance { - private readonly IStyleable _target; - private readonly StyledPropertyBase? _styledProperty; - private readonly DirectPropertyBase? _directProperty; - private readonly InstancedBinding? _binding; - private readonly Inner _inner; - private BindingValue _value; - private IDisposable? _subscription; - private IDisposable? _subscriptionTwoWay; - private IDisposable? _innerSubscription; - private bool _isActive; - - public PropertySetterBindingInstance( - IStyleable target, - StyledPropertyBase property, - IBinding binding) - { - _target = target; - _styledProperty = property; - _binding = binding.Initiate(_target, property); - - if (_binding?.Mode == BindingMode.OneTime) - { - // For the moment, we don't support OneTime bindings in setters, because I'm not - // sure what the semantics should be in the case of activation/deactivation. - throw new NotSupportedException("OneTime bindings are not supported in setters."); - } - - _inner = new Inner(this); - } + private readonly IDisposable? _twoWaySubscription; public PropertySetterBindingInstance( - IStyleable target, - DirectPropertyBase property, - IBinding binding) - { - _target = target; - _directProperty = property; - _binding = binding.Initiate(_target, property); - _inner = new Inner(this); - } - - public void Start(bool hasActivator) - { - if (_binding is null) - return; - - _isActive = !hasActivator; - - if (_styledProperty is object) - { - if (_binding.Mode != BindingMode.OneWayToSource) - { - var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; - _subscription = _target.Bind(_styledProperty, this, priority); - } - - if (_binding.Mode == BindingMode.TwoWay) - { - _subscriptionTwoWay = _target.GetBindingObservable(_styledProperty).Subscribe(this); - } - } - else - { - if (_binding.Mode != BindingMode.OneWayToSource) - { - _subscription = _target.Bind(_directProperty!, this); - } - - if (_binding.Mode == BindingMode.TwoWay) - { - _subscriptionTwoWay = _target.GetBindingObservable(_directProperty!).Subscribe(this); - } - } - } - - public void Activate() - { - if (_binding is null) - return; - - if (!_isActive) - { - _innerSubscription ??= _binding.Observable!.Subscribe(_inner); - _isActive = true; - PublishNext(); - } - } - - public void Deactivate() + AvaloniaObject target, + StyleInstance instance, + AvaloniaProperty property, + BindingMode mode, + IObservable source) + : base(instance, property, source) { - if (_isActive) + if (mode == BindingMode.TwoWay) { - _isActive = false; - _innerSubscription?.Dispose(); - _innerSubscription = null; - PublishNext(); - } - } - - public override void Dispose() - { - if (_subscription is object) - { - var sub = _subscription; - _subscription = null; - sub.Dispose(); - } - - if (_subscriptionTwoWay is object) - { - var sub = _subscriptionTwoWay; - _subscriptionTwoWay = null; - sub.Dispose(); - } - - base.Dispose(); - } - - void IObserver>.OnCompleted() - { - // This is the observable coming from the target control. It should not complete. - } - - void IObserver>.OnError(Exception error) - { - // This is the observable coming from the target control. It should not error. - } - - void IObserver>.OnNext(BindingValue value) - { - if (value.HasValue && _isActive && _binding?.Subject is not null) - { - _binding.Subject.OnNext(value.Value); - } - } - - protected override void Subscribed() - { - if (_isActive && _binding?.Observable is not null) - { - if (_innerSubscription is null) + // TODO: HUGE HACK FIXME + if (source is IObserver observer) { - _innerSubscription ??= _binding.Observable!.Subscribe(_inner); + _twoWaySubscription = target.GetObservable(property).Skip(1).Subscribe(observer); } else { - PublishNext(); + throw new NotSupportedException( + "Attempting to bind two-way with a binding source which doesn't support it."); } } } - protected override void Unsubscribed() - { - _innerSubscription?.Dispose(); - _innerSubscription = null; - } - - private void PublishNext() - { - PublishNext(_isActive ? _value : default); - } - - private void ConvertAndPublishNext(object? value) - { - _value = BindingValue.FromUntyped(value); - - if (_isActive) - { - PublishNext(); - } - } - - private class Inner : IObserver + public override void Unsubscribe() { - private readonly PropertySetterBindingInstance _owner; - public Inner(PropertySetterBindingInstance owner) => _owner = owner; - public void OnCompleted() => _owner.PublishCompleted(); - public void OnError(Exception error) => _owner.PublishError(error); - public void OnNext(object? value) => _owner.ConvertAndPublishNext(value); + _twoWaySubscription?.Dispose(); + base.Unsubscribe(); } } } diff --git a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs index 0f6efef1be..465dc21b57 100644 --- a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs @@ -1,127 +1,34 @@ using System; -using Avalonia.Data; -using Avalonia.Reactive; - -#nullable enable +using Avalonia.PropertyStore; namespace Avalonia.Styling { - /// - /// A which has been instanced on a control and whose value is lazily - /// evaluated. - /// - /// The target property type. - internal class PropertySetterTemplateInstance : SingleSubscriberObservableBase>, - ISetterInstance + internal class PropertySetterTemplateInstance : IValueEntry, ISetterInstance { - private readonly IStyleable _target; - private readonly StyledPropertyBase? _styledProperty; - private readonly DirectPropertyBase? _directProperty; private readonly ITemplate _template; - private BindingValue _value; - private IDisposable? _subscription; - private bool _isActive; - - public PropertySetterTemplateInstance( - IStyleable target, - StyledPropertyBase property, - ITemplate template) - { - _target = target; - _styledProperty = property; - _template = template; - } + private object? _value; - public PropertySetterTemplateInstance( - IStyleable target, - DirectPropertyBase property, - ITemplate template) + public PropertySetterTemplateInstance(AvaloniaProperty property, ITemplate template) { - _target = target; - _directProperty = property; _template = template; + Property = property; } - public void Start(bool hasActivator) - { - _isActive = !hasActivator; - - if (_styledProperty is not null) - { - var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; - _subscription = _target.Bind(_styledProperty, this, priority); - } - else - { - _subscription = _target.Bind(_directProperty!, this); - } - } + public bool HasValue => true; + public AvaloniaProperty Property { get; } - public void Activate() + public object? GetValue() { - if (!_isActive) - { - _isActive = true; - PublishNext(); - } + TryGetValue(out var value); + return value; } - public void Deactivate() + public bool TryGetValue(out object? value) { - if (_isActive) - { - _isActive = false; - PublishNext(); - } + value = _value ??= _template.Build(); + return value != AvaloniaProperty.UnsetValue; } - public override void Dispose() - { - if (_subscription is not null) - { - var sub = _subscription; - _subscription = null; - sub.Dispose(); - } - else if (_isActive) - { - if (_styledProperty is not null) - { - _target.ClearValue(_styledProperty); - } - else - { - _target.ClearValue(_directProperty!); - } - } - - base.Dispose(); - } - - protected override void Subscribed() => PublishNext(); - protected override void Unsubscribed() { } - - private void EnsureTemplate() - { - if (_value.HasValue) - { - return; - } - - _value = (T) _template.Build(); - } - - private void PublishNext() - { - if (_isActive) - { - EnsureTemplate(); - PublishNext(_value); - } - else - { - PublishNext(default); - } - } + void IValueEntry.Unsubscribe() { } } } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index d989bb0706..fdee64a0de 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -2,8 +2,7 @@ using System; using Avalonia.Animation; using Avalonia.Data; using Avalonia.Metadata; - -#nullable enable +using Avalonia.PropertyStore; namespace Avalonia.Styling { @@ -14,9 +13,10 @@ namespace Avalonia.Styling /// A is used to set a value on a /// depending on a condition. /// - public class Setter : ISetter, IAnimationSetter + public class Setter : ISetter, IValueEntry, ISetterInstance, IAnimationSetter { private object? _value; + private DirectPropertySetterInstance? _direct; /// /// Initializes a new instance of the class. @@ -30,7 +30,7 @@ namespace Avalonia.Styling /// /// The property to set. /// The property value. - public Setter(AvaloniaProperty property, object value) + public Setter(AvaloniaProperty property, object? value) { Property = property; Value = value; @@ -57,16 +57,78 @@ namespace Avalonia.Styling } } - public ISetterInstance Instance(IStyleable target) - { - target = target ?? throw new ArgumentNullException(nameof(target)); + bool IValueEntry.HasValue => true; + AvaloniaProperty IValueEntry.Property => EnsureProperty(); + + public override string ToString() => $"Setter: {Property} = {Value}"; + + void IValueEntry.Unsubscribe() { } + ISetterInstance ISetter.Instance(IStyleInstance instance, IStyleable target) + { + if (target is not AvaloniaObject ao) + throw new InvalidOperationException("Don't know how to instance a style on this type."); if (Property is null) - { throw new InvalidOperationException("Setter.Property must be set."); + if (Property.IsDirect && instance.HasActivator) + throw new InvalidOperationException( + $"Cannot set direct property '{Property}' in '{instance.Source}' because the style has an activator."); + + if (Value is IBinding binding) + return SetBinding((StyleInstance)instance, ao, binding); + else if (Value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(Property.PropertyType)) + return new PropertySetterTemplateInstance(Property, template); + else if (!Property.IsValidValue(Value)) + throw new InvalidCastException($"Setter value '{Value}' is not a valid value for property '{Property}'."); + else if (Property.IsDirect) + return SetDirectValue(target); + else + return this; + } + + object? IValueEntry.GetValue() => Value; + + bool IValueEntry.TryGetValue(out object? value) + { + value = Value; + return true; + } + + private AvaloniaProperty EnsureProperty() + { + return Property ?? throw new InvalidOperationException("Setter.Property must be set."); + } + + private ISetterInstance SetBinding(StyleInstance instance, AvaloniaObject target, IBinding binding) + { + if (!Property!.IsDirect) + { + var i = binding.Initiate(target, Property)!; + var mode = i.Mode; + + if (mode == BindingMode.Default) + { + mode = Property!.GetMetadata(target.GetType()).DefaultBindingMode; + } + + if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) + { + return new PropertySetterBindingInstance(target, instance, Property, mode, i.Observable!); + } + + throw new NotSupportedException(); + } + else + { + target.Bind(Property, binding); + return new DirectPropertySetterBindingInstance(); } + } - return Property.CreateSetterInstance(target, Value); + private ISetterInstance SetDirectValue(IStyleable target) + { + target.SetValue(Property!, Value); + return _direct ??= new DirectPropertySetterInstance(); } } } diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index c61b08b2a1..22c7221c45 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.PropertyStore; namespace Avalonia.Styling { @@ -7,6 +8,7 @@ namespace Avalonia.Styling /// public class Style : StyleBase { + private bool? _inControlTheme; private Selector? _selector; /// @@ -48,7 +50,9 @@ namespace Avalonia.Styling SelectorMatch.NeverThisInstance); if (match.IsMatch) + { Attach(target, match.Activator); + } result = match.Result; } @@ -95,6 +99,28 @@ namespace Avalonia.Styling base.SetParent(parent); } + private bool IsInControlTheme() + { + if (_inControlTheme.HasValue) + return _inControlTheme.Value; + + StyleBase? s = this; + + while (s is not null) + { + if (s is ControlTheme) + { + _inControlTheme = true; + return true; + } + + s = s.Parent as StyleBase; + } + + _inControlTheme = false; + return false; + } + private static Selector? ValidateSelector(Selector? selector) { if (selector is TemplateSelector) diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index 306a4cf010..a6eb938d5b 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +using Avalonia.PropertyStore; using Avalonia.Styling.Activators; namespace Avalonia.Styling @@ -80,11 +81,24 @@ namespace Avalonia.Styling return _resources?.TryGetResource(key, out result) ?? false; } - internal void Attach(IStyleable target, IStyleActivator? activator) + internal IValueFrame Attach(IStyleable target, IStyleActivator? activator) { - var instance = new StyleInstance(this, target, _setters, _animations, activator); - target.StyleApplied(instance); - instance.Start(); + if (target is not AvaloniaObject ao) + throw new InvalidOperationException("Styles can only be applied to AvaloniaObjects."); + + var instance = new StyleInstance(this, activator); + + if (_setters is object) + { + foreach (var setter in _setters) + { + var setterInstance = setter.Instance(instance, target); + instance.Add(setterInstance); + } + } + + ao.GetValueStore().AddFrame(instance); + return instance; } internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host) diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index db96da6821..e3be01b33d 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/src/Avalonia.Base/Styling/StyleInstance.cs @@ -1,137 +1,74 @@ using System; using System.Collections.Generic; -using System.Reactive.Subjects; -using Avalonia.Animation; +using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Styling.Activators; -#nullable enable - namespace Avalonia.Styling { /// - /// A which has been instanced on a control. + /// Stores state for a that has been instanced on a control. /// - internal sealed class StyleInstance : IStyleInstance, IStyleActivatorSink + /// + /// implements the interface meaning that + /// it is injected directly into the value store of an . Depending + /// on the setters present on the style, it may be possible to share a single style instance + /// among all controls that the style is applied to; meaning that a single style instance can + /// apply to multiple controls. + /// + internal class StyleInstance : ValueFrameBase, IStyleInstance, IStyleActivatorSink, IDisposable { - private readonly ISetterInstance[]? _setters; - private readonly IDisposable[]? _animations; private readonly IStyleActivator? _activator; - private readonly Subject? _animationTrigger; + private List? _setters; + private bool _isActivatorInitializing; + private bool _isActivatorSubscribed; - public StyleInstance( - IStyle source, - IStyleable target, - IReadOnlyList? setters, - IReadOnlyList? animations, - IStyleActivator? activator = null) + public StyleInstance(IStyle style, IStyleActivator? activator) { - Source = source ?? throw new ArgumentNullException(nameof(source)); - Target = target ?? throw new ArgumentNullException(nameof(target)); _activator = activator; - IsActive = _activator is null; - - if (setters is not null) - { - var setterCount = setters.Count; - - _setters = new ISetterInstance[setterCount]; + Priority = activator is object ? BindingPriority.StyleTrigger : BindingPriority.Style; + Source = style; + } - for (var i = 0; i < setterCount; ++i) - { - _setters[i] = setters[i].Instance(Target); - } - } + public bool HasActivator => _activator is object; - if (animations is not null && target is Animatable animatable) + public override bool IsActive + { + get { - var animationsCount = animations.Count; - - _animations = new IDisposable[animationsCount]; - _animationTrigger = new Subject(); - - for (var i = 0; i < animationsCount; ++i) + if (_activator is object && !_isActivatorSubscribed) { - _animations[i] = animations[i].Apply(animatable, null, _animationTrigger); + _isActivatorInitializing = true; + _activator.Subscribe(this); + _isActivatorInitializing = false; + _isActivatorSubscribed = true; } + + return _activator?.IsActive ?? true; } } - public bool HasActivator => _activator is not null; - public bool IsActive { get; private set; } + public override BindingPriority Priority { get; } public IStyle Source { get; } - public IStyleable Target { get; } - public void Start() + public void Add(ISetterInstance instance) { - var hasActivator = HasActivator; - - if (_setters is not null) - { - foreach (var setter in _setters) - { - setter.Start(hasActivator); - } - } - - if (hasActivator) - { - _activator!.Subscribe(this, 0); - } - else if (_animationTrigger is not null) - { - _animationTrigger.OnNext(true); - } + if (instance is IValueEntry valueEntry) + base.Add(valueEntry); + else + (_setters ??= new()).Add(instance); } - public void Dispose() + public override void Dispose() { - if (_setters is not null) - { - foreach (var setter in _setters) - { - setter.Dispose(); - } - } - - if (_animations is not null) - { - foreach (var subscription in _animations) - { - subscription.Dispose(); - } - } - + base.Dispose(); _activator?.Dispose(); } - private void ActivatorChanged(bool value) + void IStyleActivatorSink.OnNext(bool value, int tag) { - if (IsActive != value) - { - IsActive = value; - - _animationTrigger?.OnNext(value); - - if (_setters is not null) - { - if (IsActive) - { - foreach (var setter in _setters) - { - setter.Activate(); - } - } - else - { - foreach (var setter in _setters) - { - setter.Deactivate(); - } - } - } - } + if (!_isActivatorInitializing) + Owner?.FrameActivationChanged(this); } - - void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index cbe3771577..d9cb57baea 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -92,6 +92,8 @@ namespace Avalonia.Utilities return (0, false); } + public bool Contains(AvaloniaProperty property) => TryFindEntry(property.Id).Item2; + public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) { (int index, bool found) = TryFindEntry(property.Id); @@ -129,7 +131,12 @@ namespace Avalonia.Utilities public void SetValue(AvaloniaProperty property, TValue value) { - _entries[TryFindEntry(property.Id).Item1].Value = value; + var (index, found) = TryFindEntry(property.Id); + + if (found) + _entries[index].Value = value; + else + AddValue(property, value); } public void Remove(AvaloniaProperty property) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs deleted file mode 100644 index bf29e0b0ac..0000000000 --- a/src/Avalonia.Base/ValueStore.cs +++ /dev/null @@ -1,507 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Data; -using Avalonia.PropertyStore; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Stores styled property values for an . - /// - /// - /// At its core this class consists of an to - /// mapping which holds the current values for each set property. This - /// can be in one of 4 states: - /// - /// - For a single local value it will be an instance of . - /// - For a single value of a priority other than LocalValue it will be an instance of - /// ` - /// - For a single binding it will be an instance of - /// - For all other cases it will be an instance of - /// - internal class ValueStore - { - private readonly AvaloniaObject _owner; - private readonly AvaloniaPropertyValueStore _values; - private BatchUpdate? _batchUpdate; - - public ValueStore(AvaloniaObject owner) - { - _owner = owner; - _values = new AvaloniaPropertyValueStore(); - } - - public void BeginBatchUpdate() - { - _batchUpdate ??= new BatchUpdate(this); - _batchUpdate.Begin(); - } - - public void EndBatchUpdate() - { - if (_batchUpdate is null) - { - throw new InvalidOperationException("No batch update in progress."); - } - - if (_batchUpdate.End()) - { - _batchUpdate = null; - } - } - - public bool IsAnimating(AvaloniaProperty property) - { - if (TryGetValue(property, out var slot)) - { - return slot.Priority < BindingPriority.LocalValue; - } - - return false; - } - - public bool IsSet(AvaloniaProperty property) - { - if (TryGetValue(property, out var slot)) - { - return slot.GetValue().HasValue; - } - - return false; - } - - public bool TryGetValue( - StyledPropertyBase property, - BindingPriority maxPriority, - out T value) - { - if (TryGetValue(property, out var slot)) - { - var v = ((IValue)slot).GetValue(maxPriority); - - if (v.HasValue) - { - value = v.Value; - return true; - } - } - - value = default!; - return false; - } - - public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) - { - if (property.ValidateValue?.Invoke(value) == false) - { - throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); - } - - IDisposable? result = null; - - if (TryGetValue(property, out var slot)) - { - result = SetExisting(slot, property, value, priority); - } - else if (property.HasCoercion) - { - // If the property has any coercion callbacks then always create a PriorityValue. - var entry = new PriorityValue(_owner, property, this); - AddValue(property, entry); - result = entry.SetValue(value, priority); - } - else - { - if (priority == BindingPriority.LocalValue) - { - AddValue(property, new LocalValueEntry(value)); - NotifyValueChanged(property, default, value, priority); - } - else - { - var entry = new ConstantValueEntry(property, value, priority, new(this)); - AddValue(property, entry); - NotifyValueChanged(property, default, value, priority); - result = entry; - } - } - - return result; - } - - public IDisposable AddBinding( - StyledPropertyBase property, - IObservable> source, - BindingPriority priority) - { - if (TryGetValue(property, out var slot)) - { - return BindExisting(slot, property, source, priority); - } - else if (property.HasCoercion) - { - // If the property has any coercion callbacks then always create a PriorityValue. - var entry = new PriorityValue(_owner, property, this); - var binding = entry.AddBinding(source, priority); - AddValue(property, entry); - return binding; - } - else - { - var entry = new BindingEntry(_owner, property, source, priority, new(this)); - AddValue(property, entry); - return entry; - } - } - - public void ClearLocalValue(StyledPropertyBase property) - { - if (TryGetValue(property, out var slot)) - { - if (slot is PriorityValue p) - { - p.ClearLocalValue(); - } - else if (slot.Priority == BindingPriority.LocalValue) - { - var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? - new Optional(value) : default; - - // During batch update values can't be removed immediately because they're needed to raise - // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal - // by setting their priority to Unset. - if (!IsBatchUpdating()) - { - _values.Remove(property); - } - else if (slot is IDisposable d) - { - d.Dispose(); - } - else - { - // Local value entries are optimized and contain only a single value field to save space, - // so there's no way to mark them for removal at the end of a batch update. Instead convert - // them to a constant value entry with Unset priority in the event of a local value being - // cleared during a batch update. - var sentinel = new ConstantValueEntry(property, Optional.Empty, BindingPriority.Unset, new(this)); - _values.SetValue(property, sentinel); - } - - NotifyValueChanged(property, old, default, BindingPriority.Unset); - } - } - } - - public void CoerceValue(AvaloniaProperty property) - { - if (TryGetValue(property, out var slot)) - { - if (slot is IPriorityValue p) - { - p.UpdateEffectiveValue(); - } - } - } - - public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) - { - if (TryGetValue(property, out var slot)) - { - var slotValue = slot.GetValue(); - return new Diagnostics.AvaloniaPropertyValue( - property, - slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue, - slot.Priority, - null); - } - - return null; - } - - public void ValueChanged(AvaloniaPropertyChangedEventArgs change) - { - if (_batchUpdate is object) - { - if (change.IsEffectiveValueChange) - { - NotifyValueChanged(change.Property, change.OldValue, change.NewValue, change.Priority); - } - } - else - { - _owner.ValueChanged(change); - } - } - - public void Completed( - StyledPropertyBase property, - IPriorityValueEntry entry, - Optional oldValue) - { - // We need to include remove sentinels here so call `_values.TryGetValue` directly. - if (_values.TryGetValue(property, out var slot) && slot == entry) - { - if (_batchUpdate is null) - { - _values.Remove(property); - _owner.Completed(property, entry, oldValue); - } - else - { - _batchUpdate.ValueChanged(property, oldValue.ToObject()); - } - } - } - - private IDisposable? SetExisting( - object slot, - StyledPropertyBase property, - T value, - BindingPriority priority) - { - IDisposable? result = null; - - if (slot is IPriorityValueEntry e) - { - var priorityValue = new PriorityValue(_owner, property, this, e); - _values.SetValue(property, priorityValue); - result = priorityValue.SetValue(value, priority); - } - else if (slot is PriorityValue p) - { - result = p.SetValue(value, priority); - } - else if (slot is LocalValueEntry l) - { - if (priority == BindingPriority.LocalValue) - { - var old = l.GetValue(BindingPriority.LocalValue); - l.SetValue(value); - NotifyValueChanged(property, old, value, priority); - } - else - { - var priorityValue = new PriorityValue(_owner, property, this, l); - if (IsBatchUpdating()) - priorityValue.BeginBatchUpdate(); - result = priorityValue.SetValue(value, priority); - _values.SetValue(property, priorityValue); - } - } - else - { - throw new NotSupportedException("Unrecognised value store slot type."); - } - - return result; - } - - private IDisposable BindExisting( - object slot, - StyledPropertyBase property, - IObservable> source, - BindingPriority priority) - { - PriorityValue priorityValue; - - if (slot is IPriorityValueEntry e) - { - priorityValue = new PriorityValue(_owner, property, this, e); - - if (IsBatchUpdating()) - { - priorityValue.BeginBatchUpdate(); - } - } - else if (slot is PriorityValue p) - { - priorityValue = p; - } - else if (slot is LocalValueEntry l) - { - priorityValue = new PriorityValue(_owner, property, this, l); - } - else - { - throw new NotSupportedException("Unrecognised value store slot type."); - } - - var binding = priorityValue.AddBinding(source, priority); - _values.SetValue(property, priorityValue); - priorityValue.UpdateEffectiveValue(); - return binding; - } - - private void AddValue(AvaloniaProperty property, IValue value) - { - _values.AddValue(property, value); - if (IsBatchUpdating() && value is IBatchUpdate batch) - batch.BeginBatchUpdate(); - value.Start(); - } - - private void NotifyValueChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) - { - if (_batchUpdate is null) - { - _owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - _owner, - property, - oldValue, - newValue, - priority)); - } - else - { - _batchUpdate.ValueChanged(property, oldValue.ToObject()); - } - } - - private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true; - - private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value) - { - return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value); - } - - private static bool IsRemoveSentinel(IValue value) - { - // Local value entries are optimized and contain only a single value field to save space, - // so there's no way to mark them for removal at the end of a batch update. Instead a - // ConstantValueEntry with a priority of Unset is used as a sentinel value. - return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset; - } - - private class BatchUpdate - { - private ValueStore _owner; - private List? _notifications; - private int _batchUpdateCount; - private int _iterator = -1; - - public BatchUpdate(ValueStore owner) => _owner = owner; - - public bool IsBatchUpdating => _batchUpdateCount > 0; - - public void Begin() - { - if (_batchUpdateCount++ == 0) - { - var values = _owner._values; - - for (var i = 0; i < values.Count; ++i) - { - (values[i] as IBatchUpdate)?.BeginBatchUpdate(); - } - } - } - - public bool End() - { - if (--_batchUpdateCount > 0) - return false; - - var values = _owner._values; - - // First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed - // but notifications will still not be raised because the owner ValueStore will still have a reference - // to this batch update object. - for (var i = 0; i < values.Count; ++i) - { - (values[i] as IBatchUpdate)?.EndBatchUpdate(); - - // Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it - // does, abort and continue batch updating. - if (_batchUpdateCount > 0) - return false; - } - - if (_notifications is object) - { - // Raise all batched notifications. Doing this can cause other notifications to be added and even - // cause a new batch update to start, so we need to handle _notifications being modified by storing - // the index in field. - _iterator = 0; - - for (; _iterator < _notifications.Count; ++_iterator) - { - var entry = _notifications[_iterator]; - - if (values.TryGetValue(entry.property, out var slot)) - { - var oldValue = entry.oldValue; - var newValue = slot.GetValue(); - - // Raising this notification can cause a new batch update to be started, which in turn - // results in another change to the property. In this case we need to update the old value - // so that the *next* notification has an oldValue which follows on from the newValue - // raised here. - _notifications[_iterator] = new Notification - { - property = entry.property, - oldValue = newValue, - }; - - // Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs. - slot.RaiseValueChanged(_owner._owner, entry.property, oldValue, newValue); - - // During batch update values can't be removed immediately because they're needed to raise - // the _sink.ValueChanged notification. They instead mark themselves for removal by setting - // their priority to Unset. We need to re-read the slot here because raising ValueChanged - // could have caused it to be updated. - if (values.TryGetValue(entry.property, out var updatedSlot) && - updatedSlot.Priority == BindingPriority.Unset) - { - values.Remove(entry.property); - } - } - - // If a new batch update was started while ending this one, abort. - if (_batchUpdateCount > 0) - return false; - } - } - - _iterator = int.MaxValue - 1; - return true; - } - - public void ValueChanged(AvaloniaProperty property, Optional oldValue) - { - _notifications ??= new List(); - - for (var i = 0; i < _notifications.Count; ++i) - { - if (_notifications[i].property == property) - { - oldValue = _notifications[i].oldValue; - _notifications.RemoveAt(i); - - if (i <= _iterator) - --_iterator; - break; - } - } - - _notifications.Add(new Notification - { - property = property, - oldValue = oldValue, - }); - } - - private struct Notification - { - public AvaloniaProperty property; - public Optional oldValue; - } - } - } -} diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 8feba116f0..da3de890d8 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -552,27 +552,24 @@ namespace Avalonia BindingPriority.LocalValue); } - protected internal sealed override void LogBindingError(AvaloniaProperty property, Exception e) + internal override ParametrizedLogger? GetBindingWarningLogger( + AvaloniaProperty property, + Exception? e) { - // Don't log a binding error unless the control is attached to a logical tree. - if (((ILogical)this).IsAttachedToLogicalTree) - { - if (e is BindingChainException b && - string.IsNullOrEmpty(b.ExpressionErrorPoint) && - DataContext == null) - { - // The error occurred at the root of the binding chain and DataContext is null; - // don't log this - the DataContext probably hasn't been set up yet. - return; - } + // Don't log a binding error unless the control is attached to the logical tree. + if (!((ILogical)this).IsAttachedToLogicalTree) + return null; - Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( - this, - "Error in binding to {Target}.{Property}: {Message}", - this, - property, - e.Message); + if (e is BindingChainException b && + string.IsNullOrEmpty(b.ExpressionErrorPoint) && + DataContext == null) + { + // The error occurred at the root of the binding chain and DataContext is null; + // don't log this - the DataContext probably hasn't been set up yet. + return null; } + + return Logger.TryGet(LogEventLevel.Warning, LogArea.Binding); } /// diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index dc52cc3ae2..d815c32070 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -371,11 +371,11 @@ namespace Avalonia.Controls.Primitives { base.OnPropertyChanged(change); - if (change.Property == ThemeProperty) - { - foreach (var child in this.GetTemplateChildren()) - child.InvalidateStyles(); - } + //if (change.Property == ThemeProperty) + //{ + // foreach (var child in this.GetTemplateChildren()) + // child.InvalidateStyles(); + //} } /// diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs index f9ff8caab1..294c830c91 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs @@ -413,25 +413,33 @@ namespace Avalonia.Base.UnitTests.Animation } [Fact] - public void Transitions_Can_Re_Set_During_Batch_Update() + public void Transitions_Can_Re_Set_During_Styling() { var target = CreateTarget(); var control = CreateControl(target.Object); // Assigning and then clearing Transitions ensures we have a transition state // collection created. - control.Transitions = null; + control.ClearValue(Control.TransitionsProperty); - control.BeginBatchUpdate(); + control.GetValueStore().BeginStyling(); // Setting opacity then Transitions means that we receive the Transitions change - // after the Opacity change when EndBatchUpdate is called. - control.Opacity = 0.5; - control.Transitions = new Transitions { target.Object }; + // after the Opacity change when EndStyling is called. + var style = new Style + { + Setters = + { + new Setter(Control.OpacityProperty, 0.5), + new Setter(Control.TransitionsProperty, new Transitions { target.Object }), + } + }; + + style.TryAttach(control, control); // Which means that the transition state hasn't been initialized with the new // Transitions when the Opacity change notification gets raised here. - control.EndBatchUpdate(); + control.GetValueStore().EndStyling(); } private static IDisposable Start() diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs deleted file mode 100644 index 45de860894..0000000000 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ /dev/null @@ -1,695 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Text; -using Avalonia.Data; -using Avalonia.Layout; -using Xunit; - -namespace Avalonia.Base.UnitTests -{ - public class AvaloniaObjectTests_BatchUpdate - { - [Fact] - public void SetValue_Should_Not_Raise_Property_Changes_During_Batch_Update() - { - var target = new TestClass(); - var raised = new List(); - - target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); - target.BeginBatchUpdate(); - target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); - - Assert.Empty(raised); - } - - [Fact] - public void Binding_Should_Not_Raise_Property_Changes_During_Batch_Update() - { - var target = new TestClass(); - var observable = new TestObservable("foo"); - var raised = new List(); - - target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); - - Assert.Empty(raised); - } - - [Fact] - public void Binding_Completion_Should_Not_Raise_Property_Changes_During_Batch_Update() - { - var target = new TestClass(); - var observable = new TestObservable("foo"); - var raised = new List(); - - target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); - target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); - target.BeginBatchUpdate(); - observable.OnCompleted(); - - Assert.Empty(raised); - } - - [Fact] - public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update() - { - var target = new TestClass(); - var observable = new TestObservable("foo"); - var raised = new List(); - - var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); - target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); - target.BeginBatchUpdate(); - sub.Dispose(); - - Assert.Empty(raised); - } - - [Fact] - public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1() - { - var target = new TestClass(); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Equal("foo", target.Foo); - Assert.Null(raised[0].OldValue); - Assert.Equal("foo", raised[0].NewValue); - } - - [Fact] - public void SetValue_Change_Should_Be_Raised_After_Batch_Update_2() - { - var target = new TestClass(); - var raised = new List(); - - target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.SetValue(TestClass.FooProperty, "bar", BindingPriority.LocalValue); - target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Equal("baz", target.Foo); - } - - [Fact] - public void SetValue_Change_Should_Be_Raised_After_Batch_Update_3() - { - var target = new TestClass(); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.SetValue(TestClass.BazProperty, Orientation.Horizontal, BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Equal(TestClass.BazProperty, raised[0].Property); - Assert.Equal(Orientation.Vertical, raised[0].OldValue); - Assert.Equal(Orientation.Horizontal, raised[0].NewValue); - Assert.Equal(Orientation.Horizontal, target.Baz); - } - - [Fact] - public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update() - { - var target = new TestClass(); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); - target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); - target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(2, raised.Count); - Assert.Equal(TestClass.BarProperty, raised[0].Property); - Assert.Equal(TestClass.FooProperty, raised[1].Property); - Assert.Equal("baz", target.Foo); - Assert.Equal("bar", target.Bar); - } - - [Fact] - public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_1() - { - var target = new TestClass(); - var observable = new TestObservable("baz"); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); - target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); - target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(2, raised.Count); - Assert.Equal(TestClass.BarProperty, raised[0].Property); - Assert.Equal(TestClass.FooProperty, raised[1].Property); - Assert.Equal("baz", target.Foo); - Assert.Equal("bar", target.Bar); - } - - [Fact] - public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_2() - { - var target = new TestClass(); - var observable = new TestObservable("foo"); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); - target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); - target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(2, raised.Count); - Assert.Equal(TestClass.BarProperty, raised[0].Property); - Assert.Equal(TestClass.FooProperty, raised[1].Property); - Assert.Equal("baz", target.Foo); - Assert.Equal("bar", target.Bar); - } - - [Fact] - public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_3() - { - var target = new TestClass(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("qux"); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); - target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); - target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); - target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(2, raised.Count); - Assert.Equal(TestClass.BarProperty, raised[0].Property); - Assert.Equal(TestClass.FooProperty, raised[1].Property); - Assert.Equal("baz", target.Foo); - Assert.Equal("bar", target.Bar); - } - - [Fact] - public void Binding_Change_Should_Be_Raised_After_Batch_Update_1() - { - var target = new TestClass(); - var observable = new TestObservable("foo"); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Equal("foo", target.Foo); - Assert.Null(raised[0].OldValue); - Assert.Equal("foo", raised[0].NewValue); - } - - [Fact] - public void Binding_Change_Should_Be_Raised_After_Batch_Update_2() - { - var target = new TestClass(); - var observable1 = new TestObservable("bar"); - var observable2 = new TestObservable("baz"); - var raised = new List(); - - target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); - target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Equal("baz", target.Foo); - Assert.Equal("foo", raised[0].OldValue); - Assert.Equal("baz", raised[0].NewValue); - } - - [Fact] - public void Binding_Change_Should_Be_Raised_After_Batch_Update_3() - { - var target = new TestClass(); - var observable = new TestObservable(Orientation.Horizontal); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.Bind(TestClass.BazProperty, observable, BindingPriority.LocalValue); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Equal(TestClass.BazProperty, raised[0].Property); - Assert.Equal(Orientation.Vertical, raised[0].OldValue); - Assert.Equal(Orientation.Horizontal, raised[0].NewValue); - Assert.Equal(Orientation.Horizontal, target.Baz); - } - - [Fact] - public void Binding_Completion_Should_Be_Raised_After_Batch_Update() - { - var target = new TestClass(); - var observable = new TestObservable("foo"); - var raised = new List(); - - target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - observable.OnCompleted(); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Null(target.Foo); - Assert.Equal("foo", raised[0].OldValue); - Assert.Null(raised[0].NewValue); - Assert.Equal(BindingPriority.Unset, raised[0].Priority); - } - - [Fact] - public void Binding_Disposal_Should_Be_Raised_After_Batch_Update() - { - var target = new TestClass(); - var observable = new TestObservable("foo"); - var raised = new List(); - - var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - sub.Dispose(); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Null(target.Foo); - Assert.Equal("foo", raised[0].OldValue); - Assert.Null(raised[0].NewValue); - Assert.Equal(BindingPriority.Unset, raised[0].Priority); - } - - [Fact] - public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1() - { - var target = new TestClass(); - var raised = new List(); - - target.Foo = "foo"; - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.ClearValue(TestClass.FooProperty); - target.EndBatchUpdate(); - - Assert.Equal(1, raised.Count); - Assert.Null(target.Foo); - Assert.Equal("foo", raised[0].OldValue); - Assert.Null(raised[0].NewValue); - Assert.Equal(BindingPriority.Unset, raised[0].Priority); - } - - [Fact] - public void Bindings_Should_Be_Subscribed_Before_Batch_Update() - { - var target = new TestClass(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("bar"); - - target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); - target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); - - Assert.Equal(1, observable1.SubscribeCount); - Assert.Equal(1, observable2.SubscribeCount); - } - - [Fact] - public void Non_Active_Binding_Should_Not_Be_Subscribed_Before_Batch_Update() - { - var target = new TestClass(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("bar"); - - target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); - target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); - - Assert.Equal(1, observable1.SubscribeCount); - Assert.Equal(0, observable2.SubscribeCount); - } - - [Fact] - public void LocalValue_Bindings_Should_Be_Subscribed_During_Batch_Update() - { - var target = new TestClass(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("bar"); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - // We need to subscribe to LocalValue bindings even if we've got a batch operation - // in progress because otherwise we don't know whether the binding or a subsequent - // SetValue with local priority will win. Notifications however shouldn't be sent. - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); - target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); - - Assert.Equal(1, observable1.SubscribeCount); - Assert.Equal(1, observable2.SubscribeCount); - Assert.Empty(raised); - } - - [Fact] - public void Style_Bindings_Should_Not_Be_Subscribed_During_Batch_Update() - { - var target = new TestClass(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("bar"); - - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style); - target.Bind(TestClass.FooProperty, observable2, BindingPriority.StyleTrigger); - - Assert.Equal(0, observable1.SubscribeCount); - Assert.Equal(0, observable2.SubscribeCount); - } - - [Fact] - public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_1() - { - var target = new TestClass(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("bar"); - - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style); - target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); - target.EndBatchUpdate(); - - Assert.Equal(0, observable1.SubscribeCount); - Assert.Equal(1, observable2.SubscribeCount); - } - - [Fact] - public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_2() - { - var target = new TestClass(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("bar"); - - target.BeginBatchUpdate(); - target.Bind(TestClass.FooProperty, observable1, BindingPriority.StyleTrigger); - target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); - target.EndBatchUpdate(); - - Assert.Equal(1, observable1.SubscribeCount); - Assert.Equal(0, observable2.SubscribeCount); - } - - [Fact] - public void Change_Can_Be_Triggered_By_Ending_Batch_Update_1() - { - var target = new TestClass(); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.Foo = "foo"; - - target.PropertyChanged += (s, e) => - { - if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") - target.Bar = "bar"; - }; - - target.EndBatchUpdate(); - - Assert.Equal("foo", target.Foo); - Assert.Equal("bar", target.Bar); - Assert.Equal(2, raised.Count); - Assert.Equal(TestClass.FooProperty, raised[0].Property); - Assert.Equal(TestClass.BarProperty, raised[1].Property); - } - - [Fact] - public void Change_Can_Be_Triggered_By_Ending_Batch_Update_2() - { - var target = new TestClass(); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.Foo = "foo"; - target.Bar = "baz"; - - target.PropertyChanged += (s, e) => - { - if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") - target.Bar = "bar"; - }; - - target.EndBatchUpdate(); - - Assert.Equal("foo", target.Foo); - Assert.Equal("bar", target.Bar); - Assert.Equal(2, raised.Count); - } - - [Fact] - public void Batch_Update_Can_Be_Triggered_By_Ending_Batch_Update() - { - var target = new TestClass(); - var raised = new List(); - - target.PropertyChanged += (s, e) => raised.Add(e); - - target.BeginBatchUpdate(); - target.Foo = "foo"; - target.Bar = "baz"; - - // Simulates the following scenario: - // - A control is added to the logical tree - // - A batch update is started to apply styles - // - Ending the batch update triggers something which removes the control from the logical tree - // - A new batch update is started to detach styles - target.PropertyChanged += (s, e) => - { - if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") - { - target.BeginBatchUpdate(); - target.ClearValue(TestClass.FooProperty); - target.ClearValue(TestClass.BarProperty); - target.EndBatchUpdate(); - } - }; - - target.EndBatchUpdate(); - - Assert.Null(target.Foo); - Assert.Null(target.Bar); - Assert.Equal(2, raised.Count); - Assert.Equal(TestClass.FooProperty, raised[0].Property); - Assert.Null(raised[0].OldValue); - Assert.Equal("foo", raised[0].NewValue); - Assert.Equal(TestClass.FooProperty, raised[1].Property); - Assert.Equal("foo", raised[1].OldValue); - Assert.Null(raised[1].NewValue); - } - - [Fact] - public void Can_Set_Cleared_Value_When_Ending_Batch_Update() - { - var target = new TestClass(); - var raised = 0; - - target.Foo = "foo"; - - target.BeginBatchUpdate(); - target.ClearValue(TestClass.FooProperty); - target.PropertyChanged += (sender, e) => - { - if (e.Property == TestClass.FooProperty && e.NewValue is null) - { - target.Foo = "bar"; - ++raised; - } - }; - target.EndBatchUpdate(); - - Assert.Equal("bar", target.Foo); - Assert.Equal(1, raised); - } - - [Fact] - public void Can_Bind_Cleared_Value_When_Ending_Batch_Update() - { - var target = new TestClass(); - var raised = 0; - var notifications = new List(); - - target.Foo = "foo"; - - target.BeginBatchUpdate(); - target.ClearValue(TestClass.FooProperty); - target.PropertyChanged += (sender, e) => - { - if (e.Property == TestClass.FooProperty && e.NewValue is null) - { - target.Bind(TestClass.FooProperty, new TestObservable("bar")); - ++raised; - } - - notifications.Add(e); - }; - target.EndBatchUpdate(); - - Assert.Equal("bar", target.Foo); - Assert.Equal(1, raised); - Assert.Equal(2, notifications.Count); - Assert.Equal(null, notifications[0].NewValue); - Assert.Equal("bar", notifications[1].NewValue); - } - - [Fact] - public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update() - { - var target = new TestClass(); - var raised = 0; - var notifications = new List(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("foo"); - - target.Bind(TestClass.FooProperty, observable1); - - target.BeginBatchUpdate(); - observable1.OnCompleted(); - target.PropertyChanged += (sender, e) => - { - if (e.Property == TestClass.FooProperty && e.NewValue is null) - { - target.Bind(TestClass.FooProperty, observable2); - ++raised; - } - - notifications.Add(e); - }; - target.EndBatchUpdate(); - - Assert.Equal("foo", target.Foo); - Assert.Equal(1, raised); - Assert.Equal(2, notifications.Count); - Assert.Equal(null, notifications[0].NewValue); - Assert.Equal("foo", notifications[1].NewValue); - } - - [Fact] - public void Can_Run_Empty_Batch_Update_When_Ending_Batch_Update() - { - var target = new TestClass(); - var raised = 0; - var notifications = new List(); - - target.Foo = "foo"; - target.Bar = "bar"; - - target.BeginBatchUpdate(); - target.ClearValue(TestClass.FooProperty); - target.ClearValue(TestClass.BarProperty); - target.PropertyChanged += (sender, e) => - { - if (e.Property == TestClass.BarProperty) - { - target.BeginBatchUpdate(); - target.EndBatchUpdate(); - } - - ++raised; - }; - target.EndBatchUpdate(); - - Assert.Null(target.Foo); - Assert.Null(target.Bar); - Assert.Equal(2, raised); - } - - public class TestClass : AvaloniaObject - { - public static readonly StyledProperty FooProperty = - AvaloniaProperty.Register(nameof(Foo)); - - public static readonly StyledProperty BarProperty = - AvaloniaProperty.Register(nameof(Bar)); - - public static readonly StyledProperty BazProperty = - AvaloniaProperty.Register(nameof(Bar), Orientation.Vertical); - - public string Foo - { - get => GetValue(FooProperty); - set => SetValue(FooProperty, value); - } - - public string Bar - { - get => GetValue(BarProperty); - set => SetValue(BarProperty, value); - } - - public Orientation Baz - { - get => GetValue(BazProperty); - set => SetValue(BazProperty, value); - } - } - - public class TestObservable : ObservableBase> - { - private readonly T _value; - private IObserver> _observer; - - public TestObservable(T value) => _value = value; - - public int SubscribeCount { get; private set; } - - public void OnCompleted() => _observer.OnCompleted(); - public void OnError(Exception e) => _observer.OnError(e); - - protected override IDisposable SubscribeCore(IObserver> observer) - { - ++SubscribeCount; - _observer = observer; - observer.OnNext(_value); - return Disposable.Empty; - } - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 6b4a6f89df..35b8c4c78f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -4,18 +4,18 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; using System.Threading.Tasks; - using Avalonia.Controls; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; -using Avalonia.Utilities; using Microsoft.Reactive.Testing; using Moq; using Xunit; +#nullable enable + namespace Avalonia.Base.UnitTests { public class AvaloniaObjectTests_Binding @@ -24,11 +24,10 @@ namespace Avalonia.Base.UnitTests public void Bind_Sets_Current_Value() { var target = new Class1(); - var source = new Class1(); + var source = new BehaviorSubject>("initial"); var property = Class1.FooProperty; - source.SetValue(property, "initial"); - target.Bind(property, source.GetObservable(property)); + target.Bind(property, source); Assert.Equal("initial", target.GetValue(property)); } @@ -38,18 +37,21 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); var source = new Subject>(); - bool raised = false; + var raised = 0; target.PropertyChanged += (s, e) => - raised = e.Property == Class1.FooProperty && - (string)e.OldValue == "foodefault" && - (string)e.NewValue == "newvalue" && - e.Priority == BindingPriority.LocalValue; + { + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal("foodefault", (string?)e.OldValue); + Assert.Equal("newvalue", (string?)e.NewValue); + Assert.Equal(BindingPriority.LocalValue, e.Priority); + ++raised; + }; target.Bind(Class1.FooProperty, source); source.OnNext("newvalue"); - Assert.True(raised); + Assert.Equal(1, raised); } [Fact] @@ -71,7 +73,7 @@ namespace Avalonia.Base.UnitTests public void Setting_LocalValue_Overrides_Binding_Until_Binding_Produces_Next_Value() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); var property = Class1.FooProperty; target.Bind(property, source); @@ -81,7 +83,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(property, "bar"); Assert.Equal("bar", target.GetValue(property)); - source.OnNext("baz"); + source.OnNext("baz"); Assert.Equal("baz", target.GetValue(property)); } @@ -89,7 +91,7 @@ namespace Avalonia.Base.UnitTests public void Completing_LocalValue_Binding_Reverts_To_Default_Value_Even_When_Local_Value_Set_Earlier() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); var property = Class1.FooProperty; target.Bind(property, source); @@ -102,10 +104,10 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Completing_LocalValue_Binding_Should_Not_Revert_To_Set_LocalValue() + public void Disposing_LocalValue_Binding_Should_Not_Revert_To_Set_LocalValue() { var target = new Class1(); - var source = new BehaviorSubject("bar"); + var source = new BehaviorSubject>("bar"); target.SetValue(Class1.FooProperty, "foo"); var sub = target.Bind(Class1.FooProperty, source); @@ -117,11 +119,43 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void LocalValue_Binding_Should_Override_Style_Binding() + { + var target = new Class1(); + var source1 = new BehaviorSubject>("foo"); + var source2 = new BehaviorSubject>("bar"); + + target.Bind(Class1.FooProperty, source1, BindingPriority.Style); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + + target.Bind(Class1.FooProperty, source2, BindingPriority.LocalValue); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Style_Binding_Should_NotOverride_LocalValue_Binding() + { + var target = new Class1(); + var source1 = new BehaviorSubject>("foo"); + var source2 = new BehaviorSubject>("bar"); + + target.Bind(Class1.FooProperty, source1, BindingPriority.LocalValue); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + + target.Bind(Class1.FooProperty, source2, BindingPriority.Style); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Completing_Animation_Binding_Reverts_To_Set_LocalValue() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); var property = Class1.FooProperty; target.SetValue(property, "foo"); @@ -192,7 +226,7 @@ namespace Avalonia.Base.UnitTests var property = Class1.FooProperty; var raised = 0; - target.Bind(property, new BehaviorSubject("bar"), BindingPriority.Style); + target.Bind(property, new BehaviorSubject>("bar"), BindingPriority.Style); target.Bind(property, source); Assert.Equal("foo", target.GetValue(property)); @@ -255,18 +289,18 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Second_LocalValue_Binding_Overrides_First() + public void Second_LocalValue_Binding_Unsubscribes_First() { var property = Class1.FooProperty; var target = new Class1(); - var source1 = new Subject(); - var source2 = new Subject(); + var source1 = new Subject>(); + var source2 = new Subject>(); target.Bind(property, source1, BindingPriority.LocalValue); target.Bind(property, source2, BindingPriority.LocalValue); source1.OnNext("foo"); - Assert.Equal("foo", target.GetValue(property)); + Assert.Equal("foodefault", target.GetValue(property)); source2.OnNext("bar"); Assert.Equal("bar", target.GetValue(property)); @@ -276,12 +310,12 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Completing_Second_LocalValue_Binding_Reverts_To_First() + public void Completing_Second_LocalValue_Binding_Doesnt_Revert_To_First() { var property = Class1.FooProperty; var target = new Class1(); - var source1 = new Subject(); - var source2 = new Subject(); + var source1 = new Subject>(); + var source2 = new Subject>(); target.Bind(property, source1, BindingPriority.LocalValue); target.Bind(property, source2, BindingPriority.LocalValue); @@ -291,7 +325,7 @@ namespace Avalonia.Base.UnitTests source1.OnNext("baz"); source2.OnCompleted(); - Assert.Equal("baz", target.GetValue(property)); + Assert.Equal("foodefault", target.GetValue(property)); } [Fact] @@ -299,8 +333,8 @@ namespace Avalonia.Base.UnitTests { var property = Class1.FooProperty; var target = new Class1(); - var source1 = new Subject(); - var source2 = new Subject(); + var source1 = new Subject>(); + var source2 = new Subject>(); target.Bind(property, source1, BindingPriority.Style); target.Bind(property, source2, BindingPriority.StyleTrigger); @@ -326,7 +360,19 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Bind_To_ValueType_Accepts_UnsetValue() + public void Bind_NonGeneric_Can_Set_Null_On_Reference_Type() + { + var target = new Class1(); + var source = new BehaviorSubject(null); + var property = Class1.FooProperty; + + target.Bind(property, source); + + Assert.Null(target.GetValue(property)); + } + + [Fact] + public void LocalValue_Bind_NonGeneric_To_ValueType_Accepts_UnsetValue() { var target = new Class1(); var source = new Subject(); @@ -339,6 +385,46 @@ namespace Avalonia.Base.UnitTests Assert.False(target.IsSet(Class1.QuxProperty)); } + [Fact] + public void Style_Bind_NonGeneric_To_ValueType_Accepts_UnsetValue() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, source, BindingPriority.Style); + source.OnNext(6.7); + source.OnNext(AvaloniaProperty.UnsetValue); + + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.False(target.IsSet(Class1.QuxProperty)); + } + + [Fact] + public void LocalValue_Bind_NonGeneric_To_ValueType_Accepts_DoNothing() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(BindingOperations.DoNothing); + + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); + } + + [Fact] + public void Style_Bind_NonGeneric_To_ValueType_Accepts_DoNothing() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, source, BindingPriority.Style); + source.OnNext(6.7); + source.OnNext(BindingOperations.DoNothing); + + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); + } + [Fact] public void OneTime_Binding_Ignores_UnsetValue() { @@ -374,7 +460,7 @@ namespace Avalonia.Base.UnitTests { Class1 target = new Class1(); - target.Bind(Class2.BarProperty, Observable.Never().StartWith("foo")); + target.Bind(Class2.BarProperty, Observable.Never>().StartWith("foo")); Assert.Equal("foo", target.GetValue(Class2.BarProperty)); } @@ -404,7 +490,7 @@ namespace Avalonia.Base.UnitTests public void Observable_Is_Unsubscribed_When_Subscription_Disposed() { var scheduler = new TestScheduler(); - var source = scheduler.CreateColdObservable(); + var source = scheduler.CreateColdObservable>(); var target = new Class1(); var subscription = target.Bind(Class1.FooProperty, source); @@ -482,7 +568,7 @@ namespace Avalonia.Base.UnitTests public void Local_Binding_Overwrites_Local_Value() { var target = new Class1(); - var binding = new Subject(); + var binding = new Subject>(); target.Bind(Class1.FooProperty, binding); @@ -660,6 +746,76 @@ namespace Avalonia.Base.UnitTests } } + [Fact] + public void Untyped_LocalValue_Binding_Logs_Invalid_Value_Type() + { + var target = new Class1(); + var source = new Subject(); + var called = false; + var expectedMessageTemplate = "Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})"; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Warning && + area == LogArea.Binding && + mt == expectedMessageTemplate && + src == target && + pv[0].GetType() == typeof(Class1) && + (AvaloniaProperty)pv[1] == Class1.QuxProperty && + (Type)pv[2] == typeof(double) && + (string)pv[3] == "foo" && + (Type)pv[4] == typeof(string)) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Class1.QuxProperty, source); + source.OnNext(1.2); + source.OnNext("foo"); + + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.True(called); + } + } + + [Fact] + public void Untyped_Style_Binding_Logs_Invalid_Value_Type() + { + var target = new Class1(); + var source = new Subject(); + var called = false; + var expectedMessageTemplate = "Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})"; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Warning && + area == LogArea.Binding && + mt == expectedMessageTemplate && + src == target && + pv[0].GetType() == typeof(Class1) && + (AvaloniaProperty)pv[1] == Class1.QuxProperty && + (Type)pv[2] == typeof(double) && + (string)pv[3] == "foo" && + (Type)pv[4] == typeof(string)) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Class1.QuxProperty, source, BindingPriority.Style); + source.OnNext(1.2); + source.OnNext("foo"); + + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.True(called); + } + } + [Fact] public async Task Bind_With_Scheduler_Executes_On_Scheduler() { @@ -726,8 +882,9 @@ namespace Avalonia.Base.UnitTests public void IsAnimating_On_Property_With_Animation_Value_Returns_True() { var target = new Class1(); + var source = new BehaviorSubject>("foo"); - target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation); + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); Assert.True(target.IsAnimating(Class1.FooProperty)); } @@ -786,7 +943,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); - Assert.False(source.ValueSetterCalled); + Assert.False(source.SetterCalled); } [Fact] @@ -797,7 +954,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); - Assert.False(source.ValueSetterCalled); + Assert.False(source.SetterCalled); } [Fact] @@ -822,7 +979,7 @@ namespace Avalonia.Base.UnitTests public void Disposing_Completed_Binding_Does_Not_Throw() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); var subscription = target.Bind(Class1.FooProperty, source); source.OnCompleted(); @@ -830,68 +987,15 @@ namespace Avalonia.Base.UnitTests subscription.Dispose(); } - [Fact] - public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_With_Value() - { - var target = new Class1(); - var source = new TestTwoWayBindingViewModel() { Value = 1 }; - source.ResetSetterCalled(); - - target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); - - Assert.False(source.ValueSetterCalled); - } - - [Fact] - public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer_With_Value() - { - var target = new Class1(); - var source = new TestTwoWayBindingViewModel() { [0] = 1 }; - source.ResetSetterCalled(); - - target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); - - Assert.False(source.ValueSetterCalled); - } - - - [Fact] - public void Disposing_a_TwoWay_Binding_Should_Set_Default_Value_On_Binding_Target_But_Not_On_Source() - { - var target = new Class3(); - - // Create a source class which has a Value set to -1 and a Minimum set to -2 - var source = new TestTwoWayBindingViewModel() { Value = -1, Minimum = -2 }; - - // Reset the setter counter - source.ResetSetterCalled(); - - // 1. bind the minimum - var disposable_1 = target.Bind(Class3.MinimumProperty, new Binding("Minimum", BindingMode.TwoWay) { Source = source }); - // 2. Bind the value - var disposable_2 = target.Bind(Class3.ValueProperty, new Binding("Value", BindingMode.TwoWay) { Source = source }); - - // Dispose the minimum binding - disposable_1.Dispose(); - // Dispose the value binding - disposable_2.Dispose(); - - - // The value setter should be called here as we have disposed minimum fist and the default value of minimum is 0, so this should be changed. - Assert.True(source.ValueSetterCalled); - // The minimum value should not be changed in the source. - Assert.False(source.MinimumSetterCalled); - } - /// /// Returns an observable that returns a single value but does not complete. /// /// The type of the observable. /// The value. /// The observable. - private IObservable Single(T value) + private IObservable> Single(T value) { - return Observable.Never().StartWith(value); + return Observable.Never>().StartWith(value); } private class Class1 : AvaloniaObject @@ -918,56 +1022,6 @@ namespace Avalonia.Base.UnitTests AvaloniaProperty.Register("Bar", "bardefault"); } - private class Class3 : AvaloniaObject - { - static Class3() - { - MinimumProperty.Changed.Subscribe(x => OnMinimumChanged(x)); - } - - private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is Class3 s) - { - s.SetValue(ValueProperty, MathUtilities.Clamp(s.Value, e.NewValue.Value, double.PositiveInfinity)); - } - } - - /// - /// Defines the property. - /// - public static readonly StyledProperty ValueProperty = - AvaloniaProperty.Register(nameof(Value), 0); - - /// - /// Gets or sets the Value property - /// - public double Value - { - get { return GetValue(ValueProperty); } - set { SetValue(ValueProperty, value); } - } - - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), 0); - - /// - /// Gets or sets the minimum property - /// - public double Minimum - { - get { return GetValue(MinimumProperty); } - set { SetValue(MinimumProperty, value); } - } - - - } - - private class TestOneTimeBinding : IBinding { private IObservable _source; @@ -979,8 +1033,8 @@ namespace Avalonia.Base.UnitTests public InstancedBinding Initiate( IAvaloniaObject target, - AvaloniaProperty targetProperty, - object anchor = null, + AvaloniaProperty? targetProperty, + object? anchor = null, bool enableDataValidation = false) { return InstancedBinding.OneTime(_source); @@ -995,7 +1049,7 @@ namespace Avalonia.Base.UnitTests private double _value; - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; public double Value { @@ -1008,8 +1062,10 @@ namespace Avalonia.Base.UnitTests if (SetterInvokedCount < MaxInvokedCount) { _value = (int)value; - if (_value > 75) _value = 75; - if (_value < 25) _value = 25; + if (_value > 75) + _value = 75; + if (_value < 25) + _value = 25; } else { @@ -1032,18 +1088,7 @@ namespace Avalonia.Base.UnitTests set { _value = value; - ValueSetterCalled = true; - } - } - - private double _minimum; - public double Minimum - { - get => _minimum; - set - { - _minimum = value; - MinimumSetterCalled = true; + SetterCalled = true; } } @@ -1053,18 +1098,11 @@ namespace Avalonia.Base.UnitTests set { _value = value; - ValueSetterCalled = true; + SetterCalled = true; } } - public bool ValueSetterCalled { get; private set; } - public bool MinimumSetterCalled { get; private set; } - - public void ResetSetterCalled() - { - ValueSetterCalled = false; - MinimumSetterCalled = false; - } + public bool SetterCalled { get; private set; } } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs index 6bd29a1577..c20b75443c 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs @@ -65,53 +65,42 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void GetBaseValue_LocalValue_Ignores_Default_Value() + public void GetBaseValue_Ignores_Default_Value() { var target = new Class3(); target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); - Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).HasValue); + Assert.False(target.GetBaseValue(Class1.FooProperty).HasValue); } [Fact] - public void GetBaseValue_LocalValue_Returns_Local_Value() + public void GetBaseValue_Returns_Local_Value() { var target = new Class3(); target.SetValue(Class1.FooProperty, "local"); target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); - Assert.Equal("local", target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).Value); + Assert.Equal("local", target.GetBaseValue(Class1.FooProperty).Value); } [Fact] - public void GetBaseValue_LocalValue_Returns_Style_Value() + public void GetBaseValue_Returns_Style_Value() { var target = new Class3(); target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); - Assert.Equal("style", target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).Value); + Assert.Equal("style", target.GetBaseValue(Class1.FooProperty).Value); } [Fact] - public void GetBaseValue_Style_Ignores_LocalValue_Animated_Value() + public void GetBaseValue_Returns_Style_Value_Set_Via_Untyped_Setters() { var target = new Class3(); - target.Bind(Class1.FooProperty, new BehaviorSubject("animated"), BindingPriority.Animation); - target.SetValue(Class1.FooProperty, "local"); - Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.Style).HasValue); - } - - [Fact] - public void GetBaseValue_Style_Returns_Style_Value() - { - var target = new Class3(); - - target.SetValue(Class1.FooProperty, "local"); - target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); - target.Bind(Class1.FooProperty, new BehaviorSubject("animated"), BindingPriority.Animation); - Assert.Equal("style", target.GetBaseValue(Class1.FooProperty, BindingPriority.Style)); + target.SetValue(Class1.FooProperty, (object)"style", BindingPriority.Style); + target.SetValue(Class1.FooProperty, (object)"animated", BindingPriority.Animation); + Assert.Equal("style", target.GetBaseValue(Class1.FooProperty).Value); } private class Class1 : AvaloniaObject diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs index 9e9ae4ec74..556440178d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Data; using Xunit; namespace Avalonia.Base.UnitTests @@ -6,7 +7,17 @@ namespace Avalonia.Base.UnitTests public class AvaloniaObjectTests_Inheritance { [Fact] - public void GetValue_Returns_Inherited_Value() + public void GetValue_Returns_Inherited_Value_1() + { + Class1 parent = new Class1(); + parent.SetValue(Class1.BazProperty, "changed"); + + Class2 child = new Class2 { Parent = parent }; + Assert.Equal("changed", child.GetValue(Class1.BazProperty)); + } + + [Fact] + public void GetValue_Returns_Inherited_Value_2() { Class1 parent = new Class1(); Class2 child = new Class2 { Parent = parent }; @@ -17,7 +28,23 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Setting_InheritanceParent_Raises_PropertyChanged_When_Value_Changed_In_Parent() + public void ClearValue_Clears_Inherited_Value() + { + Class1 parent = new Class1(); + Class2 child = new Class2 { Parent = parent }; + + parent.SetValue(Class1.BazProperty, "changed"); + + Assert.Equal("changed", child.GetValue(Class1.BazProperty)); + + parent.ClearValue(Class1.BazProperty); + + Assert.Equal("bazdefault", parent.GetValue(Class1.BazProperty)); + Assert.Equal("bazdefault", child.GetValue(Class1.BazProperty)); + } + + [Fact] + public void Setting_InheritanceParent_Raises_PropertyChanged_When_Parent_Has_Value_Set() { bool raised = false; @@ -29,15 +56,17 @@ namespace Avalonia.Base.UnitTests raised = s == child && e.Property == Class1.BazProperty && (string)e.OldValue == "bazdefault" && - (string)e.NewValue == "changed"; + (string)e.NewValue == "changed" && + e.Priority == BindingPriority.Inherited; child.Parent = parent; Assert.True(raised); + Assert.Equal("changed", child.GetValue(Class1.BazProperty)); } [Fact] - public void Setting_InheritanceParent_Raises_PropertyChanged_For_Attached_Property_When_Value_Changed_In_Parent() + public void Setting_InheritanceParent_Raises_PropertyChanged_For_Attached_Property_When_Parent_Has_Value_Set() { bool raised = false; @@ -54,6 +83,7 @@ namespace Avalonia.Base.UnitTests child.Parent = parent; Assert.True(raised); + Assert.Equal("changed", child.GetValue(AttachedOwner.AttachedProperty)); } [Fact] @@ -71,6 +101,7 @@ namespace Avalonia.Base.UnitTests child.Parent = parent; Assert.False(raised); + Assert.Equal("localvalue", child.GetValue(Class1.BazProperty)); } [Fact] @@ -91,6 +122,7 @@ namespace Avalonia.Base.UnitTests parent.SetValue(Class1.BazProperty, "changed"); Assert.True(raised); + Assert.Equal("changed", child.GetValue(Class1.BazProperty)); } [Fact] @@ -111,6 +143,7 @@ namespace Avalonia.Base.UnitTests parent.SetValue(AttachedOwner.AttachedProperty, "changed"); Assert.True(raised); + Assert.Equal("changed", child.GetValue(AttachedOwner.AttachedProperty)); } [Fact] @@ -128,6 +161,85 @@ namespace Avalonia.Base.UnitTests Assert.Equal(new[] { parent, child }, result); } + [Fact] + public void Reparenting_Raises_PropertyChanged_For_Old_And_New_Inherited_Values() + { + var oldParent = new Class1(); + oldParent.SetValue(Class1.BazProperty, "oldvalue"); + + var newParent = new Class1(); + newParent.SetValue(Class1.BazProperty, "newvalue"); + + var child = new Class2 { Parent = oldParent }; + var raised = 0; + + child.PropertyChanged += (s, e) => + { + Assert.Equal(child, e.Sender); + Assert.Equal("oldvalue", e.GetOldValue()); + Assert.Equal("newvalue", e.GetNewValue()); + Assert.Equal(BindingPriority.Inherited, e.Priority); + ++raised; + }; + + child.Parent = newParent; + + Assert.Equal(1, raised); + Assert.Equal("newvalue", child.GetValue(Class1.BazProperty)); + } + + [Fact] + public void Reparenting_Raises_PropertyChanged_On_GrandChild_For_Old_And_New_Inherited_Values() + { + var oldParent = new Class1(); + oldParent.SetValue(Class1.BazProperty, "oldvalue"); + + var newParent = new Class1(); + newParent.SetValue(Class1.BazProperty, "newvalue"); + + var child = new Class2 { Parent = oldParent }; + var grandchild = new Class2 { Parent = child }; + var raised = 0; + + grandchild.PropertyChanged += (s, e) => + { + Assert.Equal(grandchild, e.Sender); + Assert.Equal("oldvalue", e.GetOldValue()); + Assert.Equal("newvalue", e.GetNewValue()); + Assert.Equal(BindingPriority.Inherited, e.Priority); + ++raised; + }; + + child.Parent = newParent; + + Assert.Equal(1, raised); + Assert.Equal("newvalue", grandchild.GetValue(Class1.BazProperty)); + } + + [Fact] + public void Reparenting_Retains_Inherited_Property_Set_On_Child() + { + var oldParent = new Class1(); + oldParent.SetValue(Class1.BazProperty, "oldvalue"); + + var newParent = new Class1(); + newParent.SetValue(Class1.BazProperty, "newvalue"); + + var child = new Class2 { Parent = oldParent }; + child.SetValue(Class1.BazProperty, "childvalue"); + + var grandchild = new Class2 { Parent = child }; + var raised = 0; + + grandchild.PropertyChanged += (s, e) => ++raised; + + child.Parent = newParent; + + Assert.Equal(0, raised); + Assert.Equal("childvalue", child.GetValue(Class1.BazProperty)); + Assert.Equal("childvalue", grandchild.GetValue(Class1.BazProperty)); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs index 7f4dcace71..6f34865aa1 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style); Assert.Equal(2, target.CoreChanges.Count); @@ -48,47 +48,12 @@ namespace Avalonia.Base.UnitTests Assert.False(change.IsEffectiveValueChange); } - [Fact] - public void OnPropertyChangedCore_Is_Called_On_All_Binding_Property_Changes() - { - var target = new Class1(); - var style = new Subject>(); - var animation = new Subject>(); - var templatedParent = new Subject>(); - - target.Bind(Class1.FooProperty, style, BindingPriority.Style); - target.Bind(Class1.FooProperty, animation, BindingPriority.Animation); - target.Bind(Class1.FooProperty, templatedParent, BindingPriority.TemplatedParent); - - style.OnNext("style1"); - templatedParent.OnNext("tp1"); - animation.OnNext("a1"); - templatedParent.OnNext("tp2"); - templatedParent.OnCompleted(); - animation.OnNext("a2"); - style.OnNext("style2"); - style.OnCompleted(); - animation.OnCompleted(); - - var changes = target.CoreChanges.Cast>(); - - Assert.Equal( - new[] { true, true, true, false, false, true, false, false, true }, - changes.Select(x => x.IsEffectiveValueChange).ToList()); - Assert.Equal( - new[] { "style1", "tp1", "a1", "tp2", "$unset", "a2", "style2", "$unset", "foodefault" }, - changes.Select(x => x.NewValue.GetValueOrDefault("$unset")).ToList()); - Assert.Equal( - new[] { "foodefault", "style1", "tp1", "$unset", "$unset", "a1", "$unset", "$unset", "a2" }, - changes.Select(x => x.OldValue.GetValueOrDefault("$unset")).ToList()); - } - [Fact] public void OnPropertyChanged_Is_Called_Only_For_Effective_Value_Changes() { var target = new Class1(); - target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style); Assert.Equal(1, target.Changes.Count); @@ -124,19 +89,13 @@ namespace Avalonia.Base.UnitTests private static AvaloniaPropertyChangedEventArgs Clone(AvaloniaPropertyChangedEventArgs change) { var e = (AvaloniaPropertyChangedEventArgs)change; - var result = new AvaloniaPropertyChangedEventArgs( + return new AvaloniaPropertyChangedEventArgs( change.Sender, e.Property, e.OldValue, e.NewValue, - change.Priority); - - if (!change.IsEffectiveValueChange) - { - result.MarkNonEffectiveValue(); - } - - return result; + change.Priority, + change.IsEffectiveValueChange); } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index c14332e1fe..e8175cf477 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Subjects; using Avalonia.Controls; +using Avalonia.Data; using Xunit; namespace Avalonia.Base.UnitTests @@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Reverts_To_DefaultValue_If_Binding_Fails_Validation() + public void Reverts_To_DefaultValue_If_LocalValue_Binding_Fails_Validation() { var target = new Class1(); var source = new Subject(); @@ -52,6 +53,31 @@ namespace Avalonia.Base.UnitTests Assert.Equal(11, target.GetValue(Class1.FooProperty)); } + [Fact] + public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source, BindingPriority.Style); + source.OnNext(150); + + Assert.Equal(11, target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Reverts_To_Lower_Priority_If_Style_Binding_Fails_Validation() + { + var target = new Class1(); + var source = new Subject(); + + target.SetValue(Class1.FooProperty, 10, BindingPriority.Style); + target.Bind(Class1.FooProperty, source, BindingPriority.StyleTrigger); + source.OnNext(150); + + Assert.Equal(10, target.GetValue(Class1.FooProperty)); + } + [Fact] public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index f3f39b465b..a51899617b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Styling; using Avalonia.Utilities; using Xunit; @@ -149,28 +150,31 @@ namespace Avalonia.Base.UnitTests internal override IDisposable RouteBind( AvaloniaObject o, - IObservable> source, + IObservable source, BindingPriority priority) { throw new NotImplementedException(); } - internal override void RouteClearValue(AvaloniaObject o) + internal override IDisposable RouteBind( + AvaloniaObject o, + IObservable> source, + BindingPriority priority) { throw new NotImplementedException(); } - internal override object RouteGetValue(AvaloniaObject o) + internal override void RouteClearValue(AvaloniaObject o) { throw new NotImplementedException(); } - internal override object RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) + internal override object RouteGetValue(AvaloniaObject o) { throw new NotImplementedException(); } - internal override void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject oldParent) + internal override object RouteGetBaseValue(AvaloniaObject o) { throw new NotImplementedException(); } @@ -183,7 +187,7 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - internal override ISetterInstance CreateSetterInstance(IStyleable target, object value) + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs deleted file mode 100644 index aa5993f3b2..0000000000 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ /dev/null @@ -1,314 +0,0 @@ -using System; -using System.Linq; -using System.Reactive.Disposables; -using Avalonia.Data; -using Avalonia.PropertyStore; -using Moq; -using Xunit; - -namespace Avalonia.Base.UnitTests -{ - public class PriorityValueTests - { - private static readonly AvaloniaObject Owner = new AvaloniaObject(); - private static readonly ValueStore ValueStore = new ValueStore(Owner); - private static readonly StyledProperty TestProperty = new StyledProperty( - "Test", - typeof(PriorityValueTests), - new StyledPropertyMetadata()); - - [Fact] - public void Constructor_Should_Set_Value_Based_On_Initial_Entry() - { - var target = new PriorityValue( - Owner, - TestProperty, - ValueStore, - new ConstantValueEntry( - TestProperty, - "1", - BindingPriority.StyleTrigger, - new(ValueStore))); - - Assert.Equal("1", target.GetValue().Value); - Assert.Equal(BindingPriority.StyleTrigger, target.Priority); - } - - [Fact] - public void GetValue_Should_Respect_MaxPriority() - { - var target = new PriorityValue( - Owner, - TestProperty, - ValueStore); - - target.SetValue("animation", BindingPriority.Animation); - target.SetValue("local", BindingPriority.LocalValue); - target.SetValue("styletrigger", BindingPriority.StyleTrigger); - target.SetValue("style", BindingPriority.Style); - - Assert.Equal("animation", target.GetValue(BindingPriority.Animation)); - Assert.Equal("local", target.GetValue(BindingPriority.LocalValue)); - Assert.Equal("styletrigger", target.GetValue(BindingPriority.StyleTrigger)); - Assert.Equal("style", target.GetValue(BindingPriority.TemplatedParent)); - Assert.Equal("style", target.GetValue(BindingPriority.Style)); - } - - [Fact] - public void SetValue_LocalValue_Should_Not_Add_Entries() - { - var target = new PriorityValue( - Owner, - TestProperty, - ValueStore); - - target.SetValue("1", BindingPriority.LocalValue); - target.SetValue("2", BindingPriority.LocalValue); - - Assert.Empty(target.Entries); - } - - [Fact] - public void SetValue_Non_LocalValue_Should_Add_Entries() - { - var target = new PriorityValue( - Owner, - TestProperty, - ValueStore); - - target.SetValue("1", BindingPriority.Style); - target.SetValue("2", BindingPriority.Animation); - - var result = target.Entries - .OfType>() - .Select(x => x.GetValue().Value) - .ToList(); - - Assert.Equal(new[] { "1", "2" }, result); - } - - [Fact] - public void Priority_Should_Be_Set() - { - var target = new PriorityValue( - Owner, - TestProperty, - ValueStore); - - Assert.Equal(BindingPriority.Unset, target.Priority); - target.SetValue("style", BindingPriority.Style); - Assert.Equal(BindingPriority.Style, target.Priority); - target.SetValue("local", BindingPriority.LocalValue); - Assert.Equal(BindingPriority.LocalValue, target.Priority); - target.SetValue("animation", BindingPriority.Animation); - Assert.Equal(BindingPriority.Animation, target.Priority); - target.SetValue("local2", BindingPriority.LocalValue); - Assert.Equal(BindingPriority.Animation, target.Priority); - } - - [Fact] - public void Binding_With_Same_Priority_Should_Be_Appended() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - var source2 = new Source("2"); - - target.AddBinding(source1, BindingPriority.LocalValue); - target.AddBinding(source2, BindingPriority.LocalValue); - - var result = target.Entries - .OfType>() - .Select(x => x.Source) - .OfType() - .Select(x => x.Id) - .ToList(); - - Assert.Equal(new[] { "1", "2" }, result); - } - - [Fact] - public void Binding_With_Higher_Priority_Should_Be_Appended() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - var source2 = new Source("2"); - - target.AddBinding(source1, BindingPriority.LocalValue); - target.AddBinding(source2, BindingPriority.Animation); - - var result = target.Entries - .OfType>() - .Select(x => x.Source) - .OfType() - .Select(x => x.Id) - .ToList(); - - Assert.Equal(new[] { "1", "2" }, result); - } - - [Fact] - public void Binding_With_Lower_Priority_Should_Be_Prepended() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - var source2 = new Source("2"); - - target.AddBinding(source1, BindingPriority.LocalValue); - target.AddBinding(source2, BindingPriority.Style); - - var result = target.Entries - .OfType>() - .Select(x => x.Source) - .OfType() - .Select(x => x.Id) - .ToList(); - - Assert.Equal(new[] { "2", "1" }, result); - } - - [Fact] - public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - var source2 = new Source("2"); - var source3 = new Source("3"); - - target.AddBinding(source1, BindingPriority.LocalValue); - target.AddBinding(source2, BindingPriority.Style); - target.AddBinding(source3, BindingPriority.Style); - - var result = target.Entries - .OfType>() - .Select(x => x.Source) - .OfType() - .Select(x => x.Id) - .ToList(); - - Assert.Equal(new[] { "2", "3", "1" }, result); - } - - [Fact] - public void Competed_Binding_Should_Be_Removed() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - var source2 = new Source("2"); - var source3 = new Source("3"); - - target.AddBinding(source1, BindingPriority.LocalValue).Start(); - target.AddBinding(source2, BindingPriority.Style).Start(); - target.AddBinding(source3, BindingPriority.Style).Start(); - source3.OnCompleted(); - - var result = target.Entries - .OfType>() - .Select(x => x.Source) - .OfType() - .Select(x => x.Id) - .ToList(); - - Assert.Equal(new[] { "2", "1" }, result); - } - - [Fact] - public void Value_Should_Come_From_Last_Entry() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - var source2 = new Source("2"); - var source3 = new Source("3"); - - target.AddBinding(source1, BindingPriority.LocalValue).Start(); - target.AddBinding(source2, BindingPriority.Style).Start(); - target.AddBinding(source3, BindingPriority.Style).Start(); - - Assert.Equal("1", target.GetValue().Value); - } - - [Fact] - public void LocalValue_Should_Override_LocalValue_Binding() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - - target.AddBinding(source1, BindingPriority.LocalValue).Start(); - target.SetValue("2", BindingPriority.LocalValue); - - Assert.Equal("2", target.GetValue().Value); - } - - [Fact] - public void LocalValue_Should_Override_Style_Binding() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - - target.AddBinding(source1, BindingPriority.Style).Start(); - target.SetValue("2", BindingPriority.LocalValue); - - Assert.Equal("2", target.GetValue().Value); - } - - [Fact] - public void LocalValue_Should_Not_Override_Animation_Binding() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - - target.AddBinding(source1, BindingPriority.Animation).Start(); - target.SetValue("2", BindingPriority.LocalValue); - - Assert.Equal("1", target.GetValue().Value); - } - - [Fact] - public void NonAnimated_Value_Should_Be_Correct_1() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - var source2 = new Source("2"); - var source3 = new Source("3"); - - target.AddBinding(source1, BindingPriority.LocalValue).Start(); - target.AddBinding(source2, BindingPriority.Style).Start(); - target.AddBinding(source3, BindingPriority.Animation).Start(); - - Assert.Equal("3", target.GetValue().Value); - Assert.Equal("1", target.GetValue(BindingPriority.LocalValue).Value); - } - - [Fact] - public void NonAnimated_Value_Should_Be_Correct_2() - { - var target = new PriorityValue(Owner, TestProperty, ValueStore); - var source1 = new Source("1"); - var source2 = new Source("2"); - var source3 = new Source("3"); - - target.AddBinding(source1, BindingPriority.Animation).Start(); - target.AddBinding(source2, BindingPriority.Style).Start(); - target.AddBinding(source3, BindingPriority.Style).Start(); - - Assert.Equal("1", target.GetValue().Value); - Assert.Equal("3", target.GetValue(BindingPriority.LocalValue).Value); - } - - private class Source : IObservable> - { - private IObserver> _observer; - - public Source(string id) => Id = id; - public string Id { get; } - - public IDisposable Subscribe(IObserver> observer) - { - _observer = observer; - observer.OnNext(Id); - return Disposable.Empty; - } - - public void OnCompleted() => _observer.OnCompleted(); - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs b/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs new file mode 100644 index 0000000000..0c87083f8d --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Reactive; +using System.Reactive.Subjects; +using Avalonia.PropertyStore; +using Avalonia.Styling; +using Microsoft.Reactive.Testing; +using Xunit; +using static Microsoft.Reactive.Testing.ReactiveTest; + +#nullable enable + +namespace Avalonia.Base.UnitTests.PropertyStore +{ + public class ValueStoreTests_Frames + { + [Fact] + public void Adding_Frame_Raises_PropertyChanged() + { + var target = new Class1(); + var subject = new BehaviorSubject("bar"); + var result = new List(); + var style = new Style + { + Setters = + { + new Setter(Class1.FooProperty, "foo"), + new Setter(Class1.BarProperty, subject.ToBinding()), + } + }; + + target.PropertyChanged += (s, e) => + { + result.Add(new(e.Property, e.OldValue, e.NewValue)); + }; + + var frame = InstanceStyle(style, target); + target.GetValueStore().AddFrame(frame); + + Assert.Equal(new PropertyChange[] + { + new(Class1.FooProperty, "foodefault", "foo"), + new(Class1.BarProperty, "bardefault", "bar"), + }, result); + } + + [Fact] + public void Removing_Frame_Raises_PropertyChanged() + { + var target = new Class1(); + var subject = new BehaviorSubject("bar"); + var result = new List(); + var style = new Style + { + Setters = + { + new Setter(Class1.FooProperty, "foo"), + new Setter(Class1.BarProperty, subject.ToBinding()), + } + }; + var frame = InstanceStyle(style, target); + target.GetValueStore().AddFrame(frame); + + target.PropertyChanged += (s, e) => + { + result.Add(new(e.Property, e.OldValue, e.NewValue)); + }; + + target.GetValueStore().RemoveFrame(frame); + + Assert.Equal(new PropertyChange[] + { + new(Class1.FooProperty, "foo", "foodefault"), + new(Class1.BarProperty, "bar", "bardefault"), + }, result); + } + + [Fact] + public void Removing_Frame_Unsubscribes_Binding() + { + var target = new Class1(); + var scheduler = new TestScheduler(); + var obs = scheduler.CreateColdObservable(OnNext(0, "bar")); + var result = new List(); + var style = new Style + { + Setters = + { + new Setter(Class1.FooProperty, "foo"), + new Setter(Class1.BarProperty, obs.ToBinding()), + } + }; + var frame = InstanceStyle(style, target); + + target.GetValueStore().AddFrame(frame); + target.GetValueStore().RemoveFrame(frame); + + Assert.Single(obs.Subscriptions); + Assert.Equal(0, obs.Subscriptions[0].Subscribe); + Assert.NotEqual(Subscription.Infinite, obs.Subscriptions[0].Unsubscribe); + } + + private static StyleInstance InstanceStyle(Style style, StyledElement target) + { + var result = new StyleInstance(style, null); + + foreach (var setter in style.Setters) + result.Add(setter.Instance(result, target)); + + return result; + } + + private class Class1 : StyledElement + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", "foodefault"); + + public static readonly StyledProperty BarProperty = + AvaloniaProperty.Register("Bar", "bardefault", true); + } + + private record PropertyChange( + AvaloniaProperty Property, + object? OldValue, + object? NewValue); + } +} diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs index b57a024f41..c3152ed01a 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs @@ -5,11 +5,14 @@ using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Converters; -using Avalonia.Diagnostics; +using Avalonia.Media; using Avalonia.Styling; +using Avalonia.UnitTests; using Moq; using Xunit; +#nullable enable + namespace Avalonia.Base.UnitTests.Styling { public class SetterTests @@ -28,13 +31,13 @@ namespace Avalonia.Base.UnitTests.Styling var control = new TextBlock(); var subject = new BehaviorSubject("foo"); var descriptor = InstancedBinding.OneWay(subject); - var binding = Mock.Of(x => x.Initiate(control, TextBlock.TextProperty, null, false) == descriptor); + var binding = Mock.Of(x => x.Initiate(control, TextBlock.TagProperty, null, false) == descriptor); var style = Mock.Of(); - var setter = new Setter(TextBlock.TextProperty, binding); + var setter = new Setter(TextBlock.TagProperty, binding); - setter.Instance(control).Start(false); + Apply(setter, control); - Assert.Equal("foo", control.Text); + Assert.Equal("foo", control.Tag); } [Fact] @@ -47,7 +50,7 @@ namespace Avalonia.Base.UnitTests.Styling var style = Mock.Of(); var setter = new Setter(TextBlock.TagProperty, binding); - setter.Instance(control).Start(false); + Apply(setter, control); Assert.Equal(null, control.Text); } @@ -60,133 +63,309 @@ namespace Avalonia.Base.UnitTests.Styling var style = Mock.Of(); var setter = new Setter(Decorator.ChildProperty, template); - setter.Instance(control).Start(false); + Apply(setter, control); Assert.IsType(control.Child); } + [Fact] + public void Can_Set_Direct_Property_In_Style_Without_Activator() + { + var control = new TextBlock(); + var target = new Setter(); + var style = new Style(x => x.Is()) + { + Setters = + { + new Setter(TextBlock.TextProperty, "foo"), + } + }; + + Apply(style, control); + + Assert.Equal("foo", control.Text); + } + + [Fact] + public void Can_Set_Direct_Property_Binding_In_Style_Without_Activator() + { + var control = new TextBlock(); + var target = new Setter(); + var source = new BehaviorSubject("foo"); + var style = new Style(x => x.Is()) + { + Setters = + { + new Setter(TextBlock.TextProperty, source.ToBinding()), + } + }; + + Apply(style, control); + + Assert.Equal("foo", control.Text); + } + + [Fact] + public void Cannot_Set_Direct_Property_Binding_In_Style_With_Activator() + { + var control = new TextBlock(); + var target = new Setter(); + var source = new BehaviorSubject("foo"); + var style = new Style(x => x.Is().Class("foo")) + { + Setters = + { + new Setter(TextBlock.TextProperty, source.ToBinding()), + } + }; + + Assert.Throws(() => Apply(style, control)); + } + + [Fact] + public void Cannot_Set_Direct_Property_In_Style_With_Activator() + { + var control = new TextBlock(); + var target = new Setter(); + var style = new Style(x => x.Is().Class("foo")) + { + Setters = + { + new Setter(TextBlock.TextProperty, "foo"), + } + }; + + Assert.Throws(() => Apply(style, control)); + } + [Fact] public void Does_Not_Call_Converter_ConvertBack_On_OneWay_Binding() { - var control = new Decorator { Name = "foo" }; - var style = Mock.Of(); + var control = new Decorator + { + Name = "foo", + Classes = { "foo" }, + }; + var binding = new Binding("Name", BindingMode.OneWay) { Converter = new TestConverter(), RelativeSource = new RelativeSource(RelativeSourceMode.Self), }; - var setter = new Setter(Decorator.TagProperty, binding); - var instance = setter.Instance(control); - instance.Start(true); - instance.Activate(); + var style = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Decorator.TagProperty, binding) + }, + }; + + Apply(style, control); Assert.Equal("foobar", control.Tag); // Issue #1218 caused TestConverter.ConvertBack to throw here. - instance.Deactivate(); + control.Classes.Remove("foo"); Assert.Null(control.Tag); } [Fact] public void Setter_Should_Apply_Value_Without_Activator_With_Style_Priority() { - var control = new Control(); - var setter = new Setter(TextBlock.TagProperty, "foo"); + var control = new Border(); + var style = new Style(x => x.OfType()) + { + Setters = + { + new Setter(Control.TagProperty, "foo"), + }, + }; + var raised = 0; - setter.Instance(control).Start(false); + control.PropertyChanged += (s, e) => + { + Assert.Equal(Control.TagProperty, e.Property); + Assert.Equal(BindingPriority.Style, e.Priority); + ++raised; + }; - Assert.Equal("foo", control.Tag); - Assert.Equal(BindingPriority.Style, control.GetDiagnostic(TextBlock.TagProperty).Priority); + Apply(style, control); + + Assert.Equal(1, raised); } [Fact] - public void Setter_Should_Apply_Value_With_Activator_As_Binding_With_StyleTrigger_Priority() + public void Setter_Should_Apply_Value_With_Activator_With_StyleTrigger_Priority() { - var control = new Canvas(); - var setter = new Setter(TextBlock.TagProperty, "foo"); + var control = new Border { Classes = { "foo" } }; + var style = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Control.TagProperty, "foo"), + }, + }; + var activator = new Subject(); + var raised = 0; + + control.PropertyChanged += (s, e) => + { + Assert.Equal(Border.TagProperty, e.Property); + Assert.Equal(BindingPriority.StyleTrigger, e.Priority); + ++raised; + }; - var instance = setter.Instance(control); - instance.Start(true); - instance.Activate(); + Apply(style, control); - Assert.Equal("foo", control.Tag); - Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority); + Assert.Equal(1, raised); } [Fact] public void Setter_Should_Apply_Binding_Without_Activator_With_Style_Priority() { - var control = new Canvas(); - var source = new { Foo = "foo" }; - var setter = new Setter(TextBlock.TagProperty, new Binding + var control = new Border { - Source = source, - Path = nameof(source.Foo), - }); + DataContext = "foo", + }; - setter.Instance(control).Start(false); + var style = new Style(x => x.OfType()) + { + Setters = + { + new Setter(Control.TagProperty, new Binding()), + }, + }; - Assert.Equal("foo", control.Tag); - Assert.Equal(BindingPriority.Style, control.GetDiagnostic(TextBlock.TagProperty).Priority); + var raised = 0; + + control.PropertyChanged += (s, e) => + { + Assert.Equal(Control.TagProperty, e.Property); + Assert.Equal(BindingPriority.Style, e.Priority); + ++raised; + }; + + Apply(style, control); + + Assert.Equal(1, raised); } [Fact] public void Setter_Should_Apply_Binding_With_Activator_With_StyleTrigger_Priority() { - var control = new Canvas(); - var source = new { Foo = "foo" }; - var setter = new Setter(TextBlock.TagProperty, new Binding + var control = new Border { - Source = source, - Path = nameof(source.Foo), - }); + Classes = { "foo" }, + DataContext = "foo", + }; - var instance = setter.Instance(control); - instance.Start(true); - instance.Activate(); + var style = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Control.TagProperty, new Binding()), + }, + }; - Assert.Equal("foo", control.Tag); - Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority); + var raised = 0; + + control.PropertyChanged += (s, e) => + { + Assert.Equal(Control.TagProperty, e.Property); + Assert.Equal(BindingPriority.StyleTrigger, e.Priority); + ++raised; + }; + + Apply(style, control); + + Assert.Equal(1, raised); } [Fact] - public void Disposing_Setter_Should_Preserve_LocalValue() + public void Direct_Property_Setter_With_TwoWay_Binding_Should_Update_Source() { - var control = new Canvas(); - var setter = new Setter(TextBlock.TagProperty, "foo"); - - var instance = setter.Instance(control); - instance.Start(true); - instance.Activate(); + using var app = UnitTestApplication.Start(TestServices.MockThreadingInterface); + var data = new Data { Foo = "foo" }; + var control = new TextBox + { + DataContext = data, + }; - control.Tag = "bar"; + var style = new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = TextBox.TextProperty, + Value = new Binding + { + Path = "Foo", + Mode = BindingMode.TwoWay + } + } + }, + }; - instance.Dispose(); + Apply(style, control); + Assert.Equal("foo", control.Text); - Assert.Equal("bar", control.Tag); + control.Text = "bar"; + Assert.Equal("bar", data.Foo); } [Fact] - public void Disposing_Binding_Setter_Should_Preserve_LocalValue() + public void Styled_Property_Setter_With_TwoWay_Binding_Should_Update_Source() { - var control = new Canvas(); - var source = new { Foo = "foo" }; - var setter = new Setter(TextBlock.TagProperty, new Binding + var data = new Data { Bar = Brushes.Red }; + var control = new Border { - Source = source, - Path = nameof(source.Foo), - }); + DataContext = data, + }; - var instance = setter.Instance(control); - instance.Start(true); - instance.Activate(); + var style = new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = new Binding + { + Path = "Bar", + Mode = BindingMode.TwoWay + } + } + }, + }; + + Apply(style, control); + Assert.Equal(Brushes.Red, control.Background); + + control.Background = Brushes.Green; + Assert.Equal(Brushes.Green, data.Bar); + } + + private void Apply(Style style, Control control) + { + style.TryAttach(control, null); + } - control.Tag = "bar"; + private void Apply(Setter setter, Control control) + { + var style = new Style(x => x.Is()) + { + Setters = { setter }, + }; - instance.Dispose(); + Apply(style, control); + } - Assert.Equal("bar", control.Tag); + private class Data + { + public string? Foo { get; set; } + public IBrush? Bar { get; set; } } private class TestConverter : IValueConverter diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs index f2dfe66054..d1d4120c9b 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Diagnostics; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; @@ -146,7 +147,7 @@ namespace Avalonia.Base.UnitTests.Styling target.Classes.Add("foo"); target.Classes.Remove("foo"); - Assert.Equal(new[] { "foodefault", "Foo", "Bar", "foodefault" }, values); + Assert.Equal(new[] { "foodefault", "Bar", "foodefault" }, values); } [Fact] @@ -463,39 +464,40 @@ namespace Avalonia.Base.UnitTests.Styling [Fact] public void Template_In_Inactive_Style_Is_Not_Built() { - var instantiationCount = 0; - var template = new FuncTemplate(() => - { - ++instantiationCount; - return new Class1(); - }); - - Styles styles = new Styles - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(Class1.ChildProperty, template), - }, - }, - - new Style(x => x.OfType()) - { - Setters = - { - new Setter(Class1.ChildProperty, template), - }, - } - }; - - var target = new Class1(); - target.BeginBatchUpdate(); - styles.TryAttach(target, null); - target.EndBatchUpdate(); - - Assert.NotNull(target.Child); - Assert.Equal(1, instantiationCount); + throw new NotImplementedException(); + ////var instantiationCount = 0; + ////var template = new FuncTemplate(() => + ////{ + //// ++instantiationCount; + //// return new Class1(); + ////}); + + ////Styles styles = new Styles + ////{ + //// new Style(x => x.OfType()) + //// { + //// Setters = + //// { + //// new Setter(Class1.ChildProperty, template), + //// }, + //// }, + + //// new Style(x => x.OfType()) + //// { + //// Setters = + //// { + //// new Setter(Class1.ChildProperty, template), + //// }, + //// } + ////}; + + ////var target = new Class1(); + ////target.BeginBatchUpdate(); + ////styles.TryAttach(target, null); + ////target.EndBatchUpdate(); + + ////Assert.NotNull(target.Child); + ////Assert.Equal(1, instantiationCount); } [Fact] @@ -702,6 +704,28 @@ namespace Avalonia.Base.UnitTests.Styling } } + [Fact] + public void DetachStyles_Should_Detach_Activator() + { + Style style = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.FooProperty, "Foo"), + }, + }; + + var target = new Class1(); + + style.TryAttach(target, null); + + Assert.Equal(1, target.Classes.ListenerCount); + + ((IStyleable)target).DetachStyles(); + + Assert.Equal(0, target.Classes.ListenerCount); + } + [Fact] public void Should_Set_Owner_On_Assigned_Resources() { diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs index bb4d590060..cf2f946b15 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs @@ -6,6 +6,7 @@ using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.LogicalTree; +using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; @@ -35,20 +36,6 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(parent, target.InheritanceParent); } - [Fact] - public void Setting_Parent_Should_Not_Set_InheritanceParent_If_Already_Set() - { - var parent = new Decorator(); - var inheritanceParent = new Decorator(); - var target = new TestControl(); - - ((ISetInheritanceParent)target).SetParent(inheritanceParent); - parent.Child = target; - - Assert.Equal(parent, target.Parent); - Assert.Equal(inheritanceParent, target.InheritanceParent); - } - [Fact] public void InheritanceParent_Should_Be_Cleared_When_Removed_From_Parent() { @@ -61,20 +48,6 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Null(target.InheritanceParent); } - [Fact] - public void InheritanceParent_Should_Be_Cleared_When_Removed_From_Parent_When_Has_Different_InheritanceParent() - { - var parent = new Decorator(); - var inheritanceParent = new Decorator(); - var target = new TestControl(); - - ((ISetInheritanceParent)target).SetParent(inheritanceParent); - parent.Child = target; - parent.Child = null; - - Assert.Null(target.InheritanceParent); - } - [Fact] public void Adding_Element_With_Null_Parent_To_Logical_Tree_Should_Throw() { @@ -126,7 +99,7 @@ namespace Avalonia.Base.UnitTests.Styling Assert.True(childRaised); Assert.True(grandchildRaised); } - + [Fact] public void AttachedToLogicalTree_Should_Be_Called_Before_Parent_Change_Signalled() { @@ -329,6 +302,8 @@ namespace Avalonia.Base.UnitTests.Styling var root = new TestRoot(); var child = new Border(); + AvaloniaLocator.CurrentMutable.BindToSelf(new Styler()); + root.Child = child; Assert.Throws(() => child.Name = "foo"); @@ -351,22 +326,28 @@ namespace Avalonia.Base.UnitTests.Styling } [Fact] - public void StyleInstance_Is_Disposed_When_Control_Removed_From_Logical_Tree() + public void Style_Is_Removed_When_Control_Removed_From_Logical_Tree() { - using (AvaloniaLocator.EnterScope()) + var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new Border(); + var root = new TestRoot { - var root = new TestRoot(); - var child = new Border(); - - root.Child = child; - - var styleInstance = new Mock(); - ((IStyleable)child).StyleApplied(styleInstance.Object); - - root.Child = null; + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Red), + } + } + }, + Child = target, + }; - styleInstance.Verify(x => x.Dispose(), Times.Once); - } + Assert.Equal(Brushes.Red, target.Background); + root.Child = null; + Assert.Null(target.Background); } [Fact] @@ -474,7 +455,7 @@ namespace Avalonia.Base.UnitTests.Styling root.DataContext = "foo"; Assert.Equal( - new[] + new[] { "begin root", "begin a1", diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 5d17808e0c..6e4c54fa05 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -5,6 +5,7 @@ Exe false + diff --git a/tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs b/tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs deleted file mode 100644 index 0c9bcf412f..0000000000 --- a/tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Media; -using Avalonia.Styling; -using BenchmarkDotNet.Attributes; - -#nullable enable - -namespace Avalonia.Benchmarks.Styling -{ - [MemoryDiagnoser] - public class ControlTheme_Apply - { - private ControlTheme _theme; - private ControlTheme _otherTheme; - private List