فهرست منبع

Initial refactor of AvaloniaObject value store.

Most (but not all) tests passing, all features mostly implemented exception coercion.
Steven Kirk 3 سال پیش
والد
کامیت
71785b73d8
84فایلهای تغییر یافته به همراه3809 افزوده شده و 3754 حذف شده
  1. 1 1
      src/Avalonia.Base/Animation/Animatable.cs
  2. 1 1
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  3. 1 0
      src/Avalonia.Base/Avalonia.Base.csproj
  4. 133 307
      src/Avalonia.Base/AvaloniaObject.cs
  5. 6 12
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  6. 19 5
      src/Avalonia.Base/AvaloniaProperty.cs
  7. 11 13
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  8. 12 16
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  9. 33 0
      src/Avalonia.Base/CollectionPolyfills.cs
  10. 5 0
      src/Avalonia.Base/Data/BindingPriority.cs
  11. 29 0
      src/Avalonia.Base/Data/BindingValue.cs
  12. 23 31
      src/Avalonia.Base/DirectPropertyBase.cs
  13. 7 0
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  14. 87 104
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  15. 169 0
      src/Avalonia.Base/PropertyStore/BindingEntry`1.cs
  16. 0 82
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  17. 25 0
      src/Avalonia.Base/PropertyStore/DictionaryPool.cs
  18. 117 0
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  19. 220 0
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  20. 0 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  21. 0 18
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  22. 0 28
      src/Avalonia.Base/PropertyStore/IValue.cs
  23. 42 0
      src/Avalonia.Base/PropertyStore/IValueEntry.cs
  24. 33 0
      src/Avalonia.Base/PropertyStore/IValueEntry`1.cs
  25. 56 0
      src/Avalonia.Base/PropertyStore/IValueFrame.cs
  26. 44 0
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  27. 60 0
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  28. 34 0
      src/Avalonia.Base/PropertyStore/InheritanceFrame.cs
  29. 59 0
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  30. 0 41
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  31. 61 0
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  32. 61 0
      src/Avalonia.Base/PropertyStore/LoggingUtils.cs
  33. 0 326
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  34. 163 0
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs
  35. 37 0
      src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs
  36. 54 0
      src/Avalonia.Base/PropertyStore/ValueFrameBase.cs
  37. 0 45
      src/Avalonia.Base/PropertyStore/ValueOwner.cs
  38. 948 0
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  39. 40 90
      src/Avalonia.Base/StyledElement.cs
  40. 39 45
      src/Avalonia.Base/StyledPropertyBase.cs
  41. 17 0
      src/Avalonia.Base/Styling/Activators/AndActivator.cs
  42. 5 0
      src/Avalonia.Base/Styling/Activators/IStyleActivator.cs
  43. 1 0
      src/Avalonia.Base/Styling/Activators/NotActivator.cs
  44. 4 4
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  45. 17 0
      src/Avalonia.Base/Styling/Activators/OrActivator.cs
  46. 10 1
      src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs
  47. 2 2
      src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs
  48. 4 4
      src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
  49. 1 0
      src/Avalonia.Base/Styling/ControlTheme.cs
  50. 6 0
      src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs
  51. 12 0
      src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs
  52. 3 3
      src/Avalonia.Base/Styling/ISetter.cs
  53. 3 31
      src/Avalonia.Base/Styling/ISetterInstance.cs
  54. 8 11
      src/Avalonia.Base/Styling/IStyleInstance.cs
  55. 0 20
      src/Avalonia.Base/Styling/IStyleable.cs
  56. 19 178
      src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs
  57. 14 107
      src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
  58. 71 9
      src/Avalonia.Base/Styling/Setter.cs
  59. 26 0
      src/Avalonia.Base/Styling/Style.cs
  60. 18 4
      src/Avalonia.Base/Styling/StyleBase.cs
  61. 40 103
      src/Avalonia.Base/Styling/StyleInstance.cs
  62. 8 1
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  63. 0 507
      src/Avalonia.Base/ValueStore.cs
  64. 15 18
      src/Avalonia.Base/Visual.cs
  65. 5 5
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  66. 15 7
      tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
  67. 0 695
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  68. 205 167
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  69. 10 21
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs
  70. 116 4
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs
  71. 5 46
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs
  72. 27 1
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
  73. 10 6
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  74. 0 314
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  75. 126 0
      tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs
  76. 249 70
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
  77. 58 34
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  78. 24 43
      tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs
  79. 1 0
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  80. 0 147
      tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs
  81. 4 3
      tests/Avalonia.Benchmarks/Styling/Style_Apply.cs
  82. 6 6
      tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs
  83. 13 8
      tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs
  84. 1 1
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

+ 1 - 1
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)
             {

+ 1 - 1
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)!;

+ 1 - 0
src/Avalonia.Base/Avalonia.Base.csproj

@@ -34,6 +34,7 @@
     <InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />

+ 133 - 307
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<AvaloniaPropertyChangedEventArgs>? _propertyChanged;
         private List<AvaloniaObject>? _inheritanceChildren;
-        private ValueStore? _values;
+        private ValueStore _values;
         private bool _batchUpdate;
 
         /// <summary>
@@ -32,6 +31,7 @@ namespace Avalonia
         public AvaloniaObject()
         {
             VerifyAccess();
+            _values = new ValueStore(this);
         }
 
         /// <summary>
@@ -59,7 +59,7 @@ namespace Avalonia
         /// <value>
         /// The inheritance parent.
         /// </value>
-        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;
-            }
-        }
-
+        /// <summary>
+        /// Returns a value indicating whether the current thread is the UI thread.
+        /// </summary>
+        /// <returns>true if the current thread is the UI thread; otherwise false.</returns>
         public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
 
+        /// <summary>
+        /// Checks that the current thread is the UI thread and throws if not.
+        /// </summary>
         public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
 
         /// <summary>
@@ -144,9 +120,9 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         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);
         }
 
         /// <summary>
@@ -232,12 +208,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="property">The property.</param>
         /// <returns>The value.</returns>
-        public object? GetValue(AvaloniaProperty property)
-        {
-            property = property ?? throw new ArgumentNullException(nameof(property));
-
-            return property.RouteGetValue(this);
-        }
+        public object? GetValue(AvaloniaProperty property) => property.RouteGetValue(this);
 
         /// <summary>
         /// Gets a <see cref="AvaloniaProperty"/> value.
@@ -247,10 +218,9 @@ namespace Avalonia
         /// <returns>The value.</returns>
         public T GetValue<T>(StyledPropertyBase<T> property)
         {
-            property = property ?? throw new ArgumentNullException(nameof(property));
+            _ = property ?? throw new ArgumentNullException(nameof(property));
             VerifyAccess();
-
-            return GetValueOrInheritedOrDefault(property);
+            return _values.GetValue(property);
         }
 
         /// <summary>
@@ -269,18 +239,10 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property, BindingPriority maxPriority)
+        public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> 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);
         }
 
         /// <summary>
@@ -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
         /// <summary>
         /// Binds a <see cref="AvaloniaProperty"/> to an observable.
         /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <param name="source">The observable.</param>
         /// <param name="priority">The priority of the binding.</param>
@@ -398,12 +354,51 @@ namespace Avalonia
         public IDisposable Bind(
             AvaloniaProperty property,
             IObservable<object?> source,
+            BindingPriority priority = BindingPriority.LocalValue) => property.RouteBind(this, source, priority);
+
+
+        /// <summary>
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="source">The observable.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>
+        /// A disposable which can be used to terminate the binding.
+        /// </returns>
+        public IDisposable Bind<T>(
+            StyledPropertyBase<T> property,
+            IObservable<object?> 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);
+        }
+
+        /// <summary>
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="source">The observable.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>
+        /// A disposable which can be used to terminate the binding.
+        /// </returns>
+        public IDisposable Bind<T>(
+            StyledPropertyBase<T> property,
+            IObservable<T> 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);
         }
 
         /// <summary>
@@ -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);
         }
 
         /// <summary>
@@ -469,29 +464,8 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         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);
         }
 
         /// <inheritdoc/>
@@ -507,98 +481,12 @@ namespace Avalonia
             _inheritanceChildren?.Remove(child);
         }
 
-        internal void InheritedPropertyChanged<T>(
-            AvaloniaProperty<T> property,
-            Optional<T> oldValue,
-            Optional<T> newValue)
-        {
-            if (property.Inherits && (_values == null || !_values.IsSet(property)))
-            {
-                RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
-            }
-        }
-
         /// <inheritdoc/>
         Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
         {
             return _propertyChanged?.GetInvocationList();
         }
 
-        internal void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
-        {
-            var property = (StyledPropertyBase<T>)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<T>(property));
-            }
-
-            if (change.IsEffectiveValueChange && !change.NewValue.HasValue)
-            {
-                change.SetNewValue(GetInheritedOrDefault(property));
-            }
-
-            if (!change.IsEffectiveValueChange ||
-                !EqualityComparer<T>.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<T>(
-            StyledPropertyBase<T> property,
-            IPriorityValueEntry entry,
-            Optional<T> oldValue) 
-        {
-            var change = new AvaloniaPropertyChangedEventArgs<T>(
-                this,
-                property,
-                oldValue,
-                default,
-                BindingPriority.Unset);
-            ValueChanged(change);
-        }
-
-        /// <summary>
-        /// Called for each inherited property when the <see cref="InheritanceParent"/> changes.
-        /// </summary>
-        /// <typeparam name="T">The type of the property value.</typeparam>
-        /// <param name="property">The property.</param>
-        /// <param name="oldParent">The old inheritance parent.</param>
-        internal void InheritanceParentChanged<T>(
-            StyledPropertyBase<T> property,
-            AvaloniaObject? oldParent)
-        {
-            var oldValue = oldParent is not null ?
-                oldParent.GetValueOrInheritedOrDefault(property) :
-                property.GetDefaultValue(GetType());
-
-            var newValue = GetInheritedOrDefault(property);
-
-            if (!EqualityComparer<T>.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<AvaloniaObject>? GetInheritanceChildren() => _inheritanceChildren;
+
         /// <summary>
-        /// Logs a binding error for a property.
+        /// Gets a logger to which a binding warning may be written.
         /// </summary>
         /// <param name="property">The property that the error occurred on.</param>
-        /// <param name="e">The binding error.</param>
-        protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e)
+        /// <param name="e">The binding exception, if any.</param>
+        /// <remarks>
+        /// This is overridden in <see cref="Visual"/> to prevent logging binding errors when a
+        /// control is not attached to the visual tree.
+        /// </remarks>
+        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);
         }
 
         /// <summary>
@@ -675,6 +567,22 @@ namespace Avalonia
         {
         }
 
+        // <summary>
+        /// Raises the <see cref="PropertyChanged"/> event for a direct property.
+        /// </summary>
+        /// <param name="property">The property that has changed.</param>
+        /// <param name="oldValue">The old property value.</param>
+        /// <param name="newValue">The new property value.</param>
+        /// <param name="priority">The priority of the binding that produced the value.</param>
+        protected void RaisePropertyChanged<T>(
+            DirectPropertyBase<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            RaisePropertyChanged(property, oldValue, newValue, priority, true);
+        }
+
         /// <summary>
         /// Raises the <see cref="PropertyChanged"/> event.
         /// </summary>
@@ -682,18 +590,43 @@ namespace Avalonia
         /// <param name="oldValue">The old property value.</param>
         /// <param name="newValue">The new property value.</param>
         /// <param name="priority">The priority of the binding that produced the value.</param>
-        protected internal void RaisePropertyChanged<T>(
+        /// <param name="isEffectiveValue">
+        /// Whether the notification represents a change to the effective value of the property.
+        /// </param>
+        internal void RaisePropertyChanged<T>(
             AvaloniaProperty<T> property,
             Optional<T> oldValue,
             BindingValue<T> newValue,
-            BindingPriority priority = BindingPriority.LocalValue)
+            BindingPriority priority,
+            bool isEffectiveValue)
         {
-            RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                this,
-                property,
-                oldValue,
-                newValue,
-                priority));
+            if (isEffectiveValue)
+                property.Notifying?.Invoke(this, true);
+
+            try
+            {
+                var e = new AvaloniaPropertyChangedEventArgs<T>(
+                    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);
+            }
         }
 
         /// <summary>
@@ -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<T>(StyledPropertyBase<T> property)
-        {
-            if (property.Inherits && InheritanceParent is AvaloniaObject o)
-            {
-                return o.GetValueOrInheritedOrDefault(property);
-            }
-
-            return property.GetDefaultValue(GetType());
-        }
-
-        private T GetValueOrInheritedOrDefault<T>(
-            StyledPropertyBase<T> 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<T>(AvaloniaPropertyChangedEventArgs<T> 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);
-                }
-            }
-        }
-
         /// <summary>
         /// Sets the value of a direct property.
         /// </summary>
@@ -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;
         }
 
-        /// <summary>
-        /// Logs a message if the notification represents a binding error.
-        /// </summary>
-        /// <param name="property">The property being bound.</param>
-        /// <param name="value">The binding notification.</param>
-        private void LogIfError<T>(AvaloniaProperty property, BindingValue<T> value)
-        {
-            if (value.HasError)
-            {
-                if (value.Error is AggregateException aggregate)
-                {
-                    foreach (var inner in aggregate.InnerExceptions)
-                    {
-                        LogBindingError(property, inner);
-                    }
-                }
-                else
-                {
-                    LogBindingError(property, value.Error!);
-                }
-            }
-        }
-
         /// <summary>
         /// Logs a property set message.
         /// </summary>

+ 6 - 12
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -362,10 +362,8 @@ namespace Avalonia
         /// </summary>
         /// <param name="target">The object.</param>
         /// <param name="property">The property.</param>
-        /// <param name="maxPriority">The maximum priority for the value.</param>
         /// <remarks>
-        /// For styled properties, gets the value of the property if set on the object with a
-        /// priority equal or lower to <paramref name="maxPriority"/>, otherwise
+        /// For styled properties, gets the value of the property excluding animated values, otherwise
         /// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return
         /// property values that come from inherited or default values.
         /// 
@@ -373,14 +371,13 @@ namespace Avalonia
         /// </remarks>
         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
         /// </summary>
         /// <param name="target">The object.</param>
         /// <param name="property">The property.</param>
-        /// <param name="maxPriority">The maximum priority for the value.</param>
         /// <remarks>
-        /// For styled properties, gets the value of the property if set on the object with a
-        /// priority equal or lower to <paramref name="maxPriority"/>, otherwise
+        /// For styled properties, gets the value of the property excluding animated values, otherwise
         /// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values
         /// that come from inherited or default values.
         /// 
@@ -400,8 +395,7 @@ namespace Avalonia
         /// </remarks>
         public static Optional<T> GetBaseValue<T>(
             this IAvaloniaObject target,
-            AvaloniaProperty<T> property,
-            BindingPriority maxPriority)
+            AvaloniaProperty<T> 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<T> styled => ao.GetBaseValue(styled, maxPriority),
+                    StyledPropertyBase<T> styled => ao.GetBaseValue(styled),
                     DirectPropertyBase<T> direct => ao.GetValue(direct),
                     _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
                 };

+ 19 - 5
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;
         }
 
+        /// <summary>
+        /// Creates an effective value for the property.
+        /// </summary>
+        /// <param name="o">The effective value owner.</param>
+        internal abstract EffectiveValue CreateEffectiveValue(AvaloniaObject o);
+
         /// <summary>
         /// Routes an untyped ClearValue call to a typed call.
         /// </summary>
@@ -471,8 +478,7 @@ namespace Avalonia
         /// Routes an untyped GetBaseValue call to a typed call.
         /// </summary>
         /// <param name="o">The object instance.</param>
-        /// <param name="maxPriority">The maximum priority for the value.</param>
-        internal abstract object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority);
+        internal abstract object? RouteGetBaseValue(AvaloniaObject o);
 
         /// <summary>
         /// Routes an untyped SetValue call to a typed call.
@@ -496,11 +502,19 @@ namespace Avalonia
         /// <param name="priority">The priority.</param>
         internal abstract IDisposable RouteBind(
             AvaloniaObject o,
-            IObservable<BindingValue<object?>> source,
+            IObservable<object?> source,
             BindingPriority priority);
 
-        internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent);
-        internal abstract ISetterInstance CreateSetterInstance(IStyleable target, object? value);
+        /// <summary>
+        /// Routes an untyped Bind call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        /// <param name="source">The binding source.</param>
+        /// <param name="priority">The priority.</param>
+        internal abstract IDisposable RouteBind(
+            AvaloniaObject o,
+            IObservable<BindingValue<object?>> source,
+            BindingPriority priority);
 
         /// <summary>
         /// Overrides the metadata for the property on the specified type.

+ 11 - 13
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;
+        }
+
         /// <summary>
         /// Gets the <see cref="AvaloniaObject"/> that the property changed on.
         /// </summary>
@@ -49,20 +59,8 @@ namespace Avalonia
         /// </value>
         public BindingPriority Priority { get; private set; }
 
-        /// <summary>
-        /// Gets a value indicating whether the change represents a change to the effective value of
-        /// the property.
-        /// </summary>
-        /// <remarks>
-        /// This will usually be true, except in
-        /// <see cref="AvaloniaObject.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs)"/>
-        /// 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.
-        /// </remarks>
-        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();

+ 12 - 16
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs

@@ -21,7 +21,18 @@ namespace Avalonia
             Optional<T> oldValue,
             BindingValue<T> newValue,
             BindingPriority priority)
-            : base(sender, priority)
+            : this(sender, property, oldValue, newValue, priority, true)
+        {
+        }
+
+        internal AvaloniaPropertyChangedEventArgs(
+            IAvaloniaObject sender,
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority,
+            bool isEffectiveValueChange)
+            : base(sender, priority, isEffectiveValueChange)
         {
             Property = property;
             OldValue = oldValue;
@@ -39,28 +50,13 @@ namespace Avalonia
         /// <summary>
         /// Gets the old value of the property.
         /// </summary>
-        /// <remarks>
-        /// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
-        /// old value of the property on the object. 
-        /// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false, returns
-        /// <see cref="Optional{T}.Empty"/>.
-        /// </remarks>
         public new Optional<T> OldValue { get; private set; }
 
         /// <summary>
         /// Gets the new value of the property.
         /// </summary>
-        /// <remarks>
-        /// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
-        /// value of the property on the object.
-        /// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false returns the
-        /// changed value, or <see cref="Optional{T}.Empty"/> if the value was removed.
-        /// </remarks>
         public new BindingValue<T> NewValue { get; private set; }
 
-        internal void SetOldValue(Optional<T> value) => OldValue = value;
-        internal void SetNewValue(BindingValue<T> value) => NewValue = value;
-
         protected override AvaloniaProperty GetProperty() => Property;
 
         protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);

+ 33 - 0
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<TKey, TValue>(
+            this Dictionary<TKey, TValue> 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<TKey, TValue>(this Dictionary<TKey, TValue> o, TKey key, TValue value)
+            where TKey : notnull
+        {
+            if (!o.ContainsKey(key))
+            {
+                o.Add(key, value);
+                return true;
+            }
+
+            return false;
+        }
+    }
+}
+#endif

+ 5 - 0
src/Avalonia.Base/Data/BindingPriority.cs

@@ -35,6 +35,11 @@ namespace Avalonia.Data
         /// A style binding.
         /// </summary>
         Style,
+        
+        /// <summary>
+        /// The value is inherited from an ancestor element.
+        /// </summary>
+        Inherited,
 
         /// <summary>
         /// The binding is uninitialized.

+ 29 - 0
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<T> x, Optional<T> y)
+        {
+            if (x.HasValue != y.HasValue)
+                return true;
+            return !EqualityComparer<T>.Default.Equals(x.Value, y.Value);
+        }
+
+        public static bool operator ==(BindingValue<T> x, Optional<T> y)
+        {
+            if (x.HasValue != y.HasValue)
+                return false;
+            return EqualityComparer<T>.Default.Equals(x.Value, y.Value);
+        }
+
+        public static bool operator !=(Optional<T> x, BindingValue<T> y)
+        {
+            if (x.HasValue != y.HasValue)
+                return true;
+            return !EqualityComparer<T>.Default.Equals(x.Value, y.Value);
+        }
+
+        public static bool operator ==(Optional<T> x, BindingValue<T> y)
+        {
+            if (x.HasValue != y.HasValue)
+                return false;
+            return EqualityComparer<T>.Default.Equals(x.Value, y.Value);
+        }
+
         /// <summary>
         /// Creates a binding value from an instance of the underlying value type.
         /// </summary>

+ 23 - 31
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.");
+        }
+
         /// <inheritdoc/>
         internal override void RouteClearValue(AvaloniaObject o)
         {
@@ -132,7 +138,7 @@ namespace Avalonia
             return o.GetValue<TValue>(this);
         }
 
-        internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority)
+        internal override object? RouteGetBaseValue(AvaloniaObject o)
         {
             return o.GetValue<TValue>(this);
         }
@@ -161,6 +167,22 @@ namespace Avalonia
             return null;
         }
 
+        /// <summary>
+        /// Routes an untyped Bind call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        /// <param name="source">The binding source.</param>
+        /// <param name="priority">The priority.</param>
+        internal override IDisposable RouteBind(
+            AvaloniaObject o,
+            IObservable<object?> source,
+            BindingPriority priority)
+        {
+            // TODO: this requires a double adapter, we should make AvaloniaObject
+            // accept an `IObservable<object?>` for direct properties directly.
+            return RouteBind(o, source.ToBindingValue(), priority);
+        }
+
         /// <inheritdoc/>
         internal override IDisposable RouteBind(
             AvaloniaObject o,
@@ -170,35 +192,5 @@ namespace Avalonia
             var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
             return o.Bind<TValue>(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<TValue>(
-                    target,
-                    this,
-                    binding);
-            }
-            else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType))
-            {
-                return new PropertySetterTemplateInstance<TValue>(
-                    target,
-                    this,
-                    template);
-            }
-            else
-            {
-                return new PropertySetterInstance<TValue>(
-                    target,
-                    this,
-                    (TValue)value!);
-            }
-        }
     }
 }

+ 7 - 0
src/Avalonia.Base/IStyledPropertyAccessor.cs

@@ -15,5 +15,12 @@ namespace Avalonia
         /// The default value.
         /// </returns>
         object? GetDefaultValue(Type type);
+
+        /// <summary>
+        /// Validates the specified property value.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        /// <returns>True if the value is valid, otherwise false.</returns>
+        bool ValidateValue(object? value);
     }
 }

+ 87 - 104
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
 {
-    /// <summary>
-    /// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
-    /// </summary>
-    internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
+    internal class BindingEntry : IValueEntry,
+        IObserver<object?>,
+        IDisposable
     {
-        void Start(bool ignoreBatchUpdate);
-    }
-
-    /// <summary>
-    /// Stores a binding in a <see cref="ValueStore"/> or <see cref="PriorityValue{T}"/>.
-    /// </summary>
-    /// <typeparam name="T">The property type.</typeparam>
-    internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>>
-    {
-        private readonly AvaloniaObject _owner;
-        private ValueOwner<T> _sink;
+        private readonly ValueFrameBase _frame;
+        private readonly IObservable<object?> _source;
         private IDisposable? _subscription;
-        private bool _isSubscribed;
-        private bool _batchUpdate;
-        private Optional<T> _value;
+        private bool _hasValue;
+        private object? _value;
 
         public BindingEntry(
-            AvaloniaObject owner,
-            StyledPropertyBase<T> property,
-            IObservable<BindingValue<T>> source,
-            BindingPriority priority,
-            ValueOwner<T> sink)
+            ValueFrameBase frame,
+            AvaloniaProperty property,
+            IObservable<object?> source)
         {
-            _owner = owner;
+            _frame = frame;
+            _source = source;
             Property = property;
-            Source = source;
-            Priority = priority;
-            _sink = sink;
         }
 
-        public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; private set; }
-        public IObservable<BindingValue<T>> Source { get; }
-        Optional<object?> 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<T> GetValue(BindingPriority maxPriority)
-        {
-            return Priority >= maxPriority ? _value : Optional<T>.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<T> 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<T> parent) => _sink = new(parent);
-
-        public void RaiseValueChanged(
-            AvaloniaObject owner,
-            AvaloniaProperty property,
-            Optional<object?> oldValue,
-            Optional<object?> newValue)
+        private void ClearValue()
         {
-            owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                owner,
-                (AvaloniaProperty<T>)property,
-                oldValue.Cast<T>(),
-                newValue.Cast<T>(),
-                Priority));
+            if (_hasValue)
+            {
+                _hasValue = false;
+                _value = default;
+                _frame.Owner?.OnBindingValueCleared(Property, _frame.Priority);
+            }
         }
 
-        private void UpdateValue(BindingValue<T> 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<T>(_owner, Property, old, value, Priority));
+        private void BindingCompleted()
+        {
+            _subscription = null;
+            _frame.OnBindingCompleted(this);
+        }
+
+        private void StartIfNecessary()
+        {
+            if (_subscription is null)
+                Start();
         }
     }
 }

+ 169 - 0
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<T> : IValueEntry<T>,
+        IObserver<T>,
+        IObserver<BindingValue<T>>,
+        IDisposable
+    {
+        private readonly ValueFrameBase _frame;
+        private readonly object _source;
+        private IDisposable? _subscription;
+        private bool _hasValue;
+        private T? _value;
+
+        public BindingEntry(
+            ValueFrameBase frame,
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source)
+        {
+            _frame = frame;
+            _source = source;
+            Property = property;
+        }
+
+        public BindingEntry(
+            ValueFrameBase frame,
+            StyledPropertyBase<T> property,
+            IObservable<T> source)
+        {
+            _frame = frame;
+            _source = source;
+            Property = property;
+        }
+
+        public bool HasValue
+        {
+            get
+            {
+                StartIfNecessary();
+                return _hasValue;
+            }
+        }
+
+        public StyledPropertyBase<T> 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<BindingValue<T>> bv)
+                _subscription = bv.Subscribe(this);
+            else if (_source is IObservable<T> 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<T> 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<T> 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<T>.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();
+        }
+    }
+}

+ 0 - 82
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -1,82 +0,0 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using Avalonia.Data;
-
-namespace Avalonia.PropertyStore
-{
-    /// <summary>
-    /// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
-    /// </summary>
-    internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
-    {
-    }
-
-    /// <summary>
-    /// Stores a value with a priority in a <see cref="ValueStore"/> or
-    /// <see cref="PriorityValue{T}"/>.
-    /// </summary>
-    /// <typeparam name="T">The property type.</typeparam>
-    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
-    {
-        private ValueOwner<T> _sink;
-        private Optional<T> _value;
-
-        public ConstantValueEntry(
-            StyledPropertyBase<T> property,
-            T value,
-            BindingPriority priority,
-            ValueOwner<T> sink)
-        {
-            Property = property;
-            _value = value;
-            Priority = priority;
-            _sink = sink;
-        }
-
-        public ConstantValueEntry(
-            StyledPropertyBase<T> property,
-            Optional<T> value,
-            BindingPriority priority,
-            ValueOwner<T> sink)
-        {
-            Property = property;
-            _value = value;
-            Priority = priority;
-            _sink = sink;
-        }
-
-        public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; private set; }
-        Optional<object?> IValue.GetValue() => _value.ToObject();
-
-        public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
-        {
-            return Priority >= maxPriority ? _value : Optional<T>.Empty;
-        }
-
-        public void Dispose()
-        {
-            var oldValue = _value;
-            _value = default;
-            Priority = BindingPriority.Unset;
-            _sink.Completed(Property, this, oldValue);
-        }
-
-        public void Reparent(PriorityValue<T> sink) => _sink = new(sink);
-        public void Start() { }
-
-        public void RaiseValueChanged(
-            AvaloniaObject owner,
-            AvaloniaProperty property,
-            Optional<object?> oldValue,
-            Optional<object?> newValue)
-        {
-            owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                owner,
-                (AvaloniaProperty<T>)property,
-                oldValue.Cast<T>(),
-                newValue.Cast<T>(),
-                Priority));
-        }
-    }
-}

+ 25 - 0
src/Avalonia.Base/PropertyStore/DictionaryPool.cs

@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace Avalonia.PropertyStore
+{
+    internal static class DictionaryPool<TKey, TValue>
+        where TKey : notnull
+    {
+        private const int MaxPoolSize = 4;
+        private static Stack<Dictionary<TKey, TValue>> _pool = new();
+
+        public static Dictionary<TKey, TValue> Get()
+        {
+            return _pool.Count == 0 ? new() : _pool.Pop();
+        }
+
+        public static void Release(Dictionary<TKey, TValue> dictionary)
+        {
+            if (_pool.Count < MaxPoolSize)
+            {
+                dictionary.Clear();
+                _pool.Push(dictionary);
+            }
+        }
+    }
+}

+ 117 - 0
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@@ -0,0 +1,117 @@
+using System;
+using Avalonia.Data;
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Represents the active value for a property in a <see cref="ValueStore"/>.
+    /// </summary>
+    /// <remarks>
+    /// This class is an abstract base for the generic <see cref="EffectiveValue{T}"/>.
+    /// </remarks>
+    internal abstract class EffectiveValue
+    {
+        /// <summary>
+        /// Gets the current effective value as a boxed value.
+        /// </summary>
+        public object? Value => GetBoxedValue();
+
+        /// <summary>
+        /// Gets the current effective base value as a boxed value, or 
+        /// <see cref="AvaloniaProperty.UnsetValue"/> if not set.
+        /// </summary>
+        public object? BaseValue => GetBoxedBaseValue();
+
+        /// <summary>
+        /// Gets the priority of the current effective value.
+        /// </summary>
+        public BindingPriority Priority { get; protected set; }
+
+        /// <summary>
+        /// Gets the priority of the current base value.
+        /// </summary>
+        public BindingPriority BasePriority { get; protected set; }
+
+        /// <summary>
+        /// Sets the value and base value, raising <see cref="AvaloniaObject.PropertyChanged"/>
+        /// where necessary.
+        /// </summary>
+        /// <param name="owner">The associated value store.</param>
+        /// <param name="property">The property being changed.</param>
+        /// <param name="value">The new value of the property.</param>
+        /// <param name="priority">The priority of the new value.</param>
+        public abstract void SetAndRaise(
+            ValueStore owner,
+            AvaloniaProperty property,
+            object? value,
+            BindingPriority priority);
+
+        /// <summary>
+        /// Sets the value and base value, raising <see cref="AvaloniaObject.PropertyChanged"/>
+        /// where necessary.
+        /// </summary>
+        /// <param name="owner">The associated value store.</param>
+        /// <param name="property">The property being changed.</param>
+        /// <param name="value">The new value of the property.</param>
+        /// <param name="priority">The priority of the new value.</param>
+        /// <param name="baseValue">The new base value of the property.</param>
+        /// <param name="basePriority">The priority of the new base value.</param>
+        public abstract void SetAndRaise(
+            ValueStore owner,
+            AvaloniaProperty property,
+            object? value,
+            BindingPriority priority,
+            object? baseValue,
+            BindingPriority basePriority);
+
+        /// <summary>
+        /// Sets the value, raising <see cref="AvaloniaObject.PropertyChanged"/>
+        /// where necessary.
+        /// </summary>
+        /// <param name="owner">The associated value store.</param>
+        /// <param name="entry">The value entry with the new value of the property.</param>
+        /// <param name="priority">The priority of the new value.</param>
+        /// <remarks>
+        /// This method does not set the base value.
+        /// </remarks>
+        public abstract void SetAndRaise(
+            ValueStore owner,
+            IValueEntry entry,
+            BindingPriority priority);
+
+        /// <summary>
+        /// Set the value priority, but leaves the value unchanged.
+        /// </summary>
+        public void SetPriority(BindingPriority priority) => Priority = BindingPriority.Unset;
+
+        /// <summary>
+        /// Set the base value priority, but leaves the base value unchanged.
+        /// </summary>
+        public void SetBasePriority(BindingPriority priority) => BasePriority = BindingPriority.Unset;
+
+        /// <summary>
+        /// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value
+        /// change.
+        /// </summary>
+        /// <param name="owner">The owner object.</param>
+        /// <param name="property">The property being changed.</param>
+        /// <param name="oldValue">The old value of the property.</param>
+        /// <param name="newValue">The new value of the property.</param>
+        public abstract void RaiseInheritedValueChanged(
+            AvaloniaObject owner,
+            AvaloniaProperty property,
+            EffectiveValue? oldValue,
+            EffectiveValue? newValue);
+
+        /// <summary>
+        /// Disposes the effective value, raising <see cref="AvaloniaObject.PropertyChanged"/>
+        /// where necessary.
+        /// </summary>
+        /// <param name="owner">The associated value store.</param>
+        /// <param name="property">The property being cleared.</param>
+        public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property);
+
+        protected abstract object? GetBoxedValue();
+        protected abstract object? GetBoxedBaseValue();
+    }
+}

+ 220 - 0
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
+{
+    /// <summary>
+    /// Represents the active value for a property in a <see cref="ValueStore"/>.
+    /// </summary>
+    /// <remarks>
+    /// Stores the active value in an <see cref="AvaloniaObject"/>'s <see cref="ValueStore"/>
+    /// for a single property, when the value is not inherited or unset/default.
+    /// </remarks>
+    internal sealed class EffectiveValue<T> : 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;
+            }
+        }
+
+        /// <summary>
+        /// Gets the current effective value.
+        /// </summary>
+        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<T>)property, (T)value!, priority);
+        }
+
+        public override void SetAndRaise(
+            ValueStore owner,
+            AvaloniaProperty property,
+            object? value,
+            BindingPriority priority,
+            object? baseValue,
+            BindingPriority basePriority)
+        {
+            SetAndRaise(owner, (StyledPropertyBase<T>)property, (T)value!, priority, (T)baseValue!, basePriority);
+        }
+
+        public override void SetAndRaise(
+            ValueStore owner,
+            IValueEntry entry,
+            BindingPriority priority)
+        {
+            var value = entry is IValueEntry<T> typed ? typed.GetValue() : (T)entry.GetValue()!;
+            SetAndRaise(owner, (StyledPropertyBase<T>)entry.Property, value, priority);
+        }
+
+        /// <summary>
+        /// Sets the value and base value, raising <see cref="AvaloniaObject.PropertyChanged"/>
+        /// where necessary.
+        /// </summary>
+        /// <param name="owner">The object on which to raise events.</param>
+        /// <param name="property">The property being changed.</param>
+        /// <param name="value">The new value of the property.</param>
+        /// <param name="priority">The priority of the new value.</param>
+        public void SetAndRaise(
+            ValueStore owner,
+            StyledPropertyBase<T> property,
+            T value,
+            BindingPriority priority)
+        {
+            Debug.Assert(priority < BindingPriority.Inherited);
+
+            var oldValue = Value;
+            var valueChanged = false;
+            var baseValueChanged = false;
+
+            if (priority <= Priority)
+            {
+                valueChanged = !EqualityComparer<T>.Default.Equals(Value, value);
+                Value = value;
+                Priority = priority;
+            }
+
+            if (priority <= BasePriority && priority >= BindingPriority.LocalValue)
+            {
+                baseValueChanged = !EqualityComparer<T>.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);
+            }
+        }
+
+        /// <summary>
+        /// Sets the value and base value, raising <see cref="AvaloniaObject.PropertyChanged"/>
+        /// where necessary.
+        /// </summary>
+        /// <param name="owner">The object on which to raise events.</param>
+        /// <param name="property">The property being changed.</param>
+        /// <param name="value">The new value of the property.</param>
+        /// <param name="priority">The priority of the new value.</param>
+        /// <param name="baseValue">The new base value of the property.</param>
+        /// <param name="basePriority">The priority of the new base value.</param>
+        public void SetAndRaise(
+            ValueStore owner,
+            StyledPropertyBase<T> 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<T>.Default.Equals(Value, value))
+            {
+                Value = value;
+                valueChanged = true;
+            }
+
+            if (BasePriority == BindingPriority.Unset || 
+                !EqualityComparer<T>.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<T>)property;
+            var o = oldValue is not null ? ((EffectiveValue<T>)oldValue).Value : p.GetDefaultValue(owner.GetType());
+            var n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : p.GetDefaultValue(owner.GetType());
+            var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset;
+
+            if (!EqualityComparer<T>.Default.Equals(o, n))
+            {
+                owner.RaisePropertyChanged(p, o, n, priority, true);
+            }
+        }
+
+        public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
+        {
+            DisposeAndRaiseUnset(owner, (StyledPropertyBase<T>)property);
+        }
+
+        public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase<T> property)
+        {
+            var defaultValue = property.GetDefaultValue(owner.GetType());
+
+            if (!EqualityComparer<T>.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;
+        }
+    }
+}

+ 0 - 8
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

@@ -1,8 +0,0 @@
-namespace Avalonia.PropertyStore
-{
-    internal interface IBatchUpdate
-    {
-        void BeginBatchUpdate();
-        void EndBatchUpdate();
-    }
-}

+ 0 - 18
src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs

@@ -1,18 +0,0 @@
-namespace Avalonia.PropertyStore
-{
-    /// <summary>
-    /// Represents an untyped interface to <see cref="IPriorityValueEntry{T}"/>.
-    /// </summary>
-    internal interface IPriorityValueEntry : IValue
-    {
-    }
-
-    /// <summary>
-    /// Represents an object that can act as an entry in a <see cref="PriorityValue{T}"/>.
-    /// </summary>
-    /// <typeparam name="T">The property type.</typeparam>
-    internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T>
-    {
-        void Reparent(PriorityValue<T> parent);
-    }
-}

+ 0 - 28
src/Avalonia.Base/PropertyStore/IValue.cs

@@ -1,28 +0,0 @@
-using Avalonia.Data;
-
-namespace Avalonia.PropertyStore
-{
-    /// <summary>
-    /// Represents an untyped interface to <see cref="IValue{T}"/>.
-    /// </summary>
-    internal interface IValue
-    {
-        BindingPriority Priority { get; }
-        Optional<object?> GetValue();
-        void Start();
-        void RaiseValueChanged(
-            AvaloniaObject owner,
-            AvaloniaProperty property,
-            Optional<object?> oldValue,
-            Optional<object?> newValue);
-    }
-
-    /// <summary>
-    /// Represents an object that can act as an entry in a <see cref="ValueStore"/>.
-    /// </summary>
-    /// <typeparam name="T">The property type.</typeparam>
-    internal interface IValue<T> : IValue
-    {
-        Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation);
-    }
-}

+ 42 - 0
src/Avalonia.Base/PropertyStore/IValueEntry.cs

@@ -0,0 +1,42 @@
+using System;
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Represents an untyped value entry in an <see cref="IValueFrame"/>.
+    /// </summary>
+    internal interface IValueEntry
+    {
+        bool HasValue { get; }
+
+        /// <summary>
+        /// Gets the property that this value applies to.
+        /// </summary>
+        AvaloniaProperty Property { get; }
+
+        /// <summary>
+        /// Gets the value associated with the entry.
+        /// </summary>
+        /// <exception cref="AvaloniaInternalException">
+        /// The entry has no value.
+        /// </exception>
+        object? GetValue();
+
+        /// <summary>
+        /// Tries to get the value associated with the entry.
+        /// </summary>
+        /// <param name="value">
+        /// When this method returns, contains the value associated with the entry if a value is
+        /// present; otherwise, returns null.
+        /// </param>
+        /// <returns>
+        /// true if the entry has an associated value; otherwise false.
+        /// </returns>
+        bool TryGetValue(out object? value);
+
+        /// <summary>
+        /// Called when the value entry is removed from the value store.
+        /// </summary>
+        void Unsubscribe();
+    }
+}

+ 33 - 0
src/Avalonia.Base/PropertyStore/IValueEntry`1.cs

@@ -0,0 +1,33 @@
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Represents a typed value entry in an <see cref="IValueFrame"/>.
+    /// </summary>
+    internal interface IValueEntry<T> : IValueEntry
+    {
+        /// <summary>
+        /// Gets the property that this value applies to.
+        /// </summary>
+        new StyledPropertyBase<T> Property { get; }
+
+        /// <summary>
+        /// Gets the value associated with the entry.
+        /// </summary>
+        /// <exception cref="AvaloniaInternalException">
+        /// The entry has no value.
+        /// </exception>
+        new T GetValue();
+
+        /// <summary>
+        /// Tries to get the value associated with the entry.
+        /// </summary>
+        /// <param name="value">
+        /// When this method returns, contains the value associated with the entry if a value is
+        /// present; otherwise, returns the default value of <typeparamref name="T"/>.
+        /// </param>
+        /// <returns>
+        /// true if the entry has an associated value; otherwise false.
+        /// </returns>
+        bool TryGetValue(out T? value);
+    }
+}

+ 56 - 0
src/Avalonia.Base/PropertyStore/IValueFrame.cs

@@ -0,0 +1,56 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Data;
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Represents a collection of property values in a <see cref="PropertyStore.ValueStore"/>.
+    /// </summary>
+    /// <remarks>
+    /// A value frame is an abstraction over the following sources of values in an
+    /// <see cref="AvaloniaObject"/>:
+    /// 
+    /// - A style
+    /// - Local values
+    /// - Animation values
+    /// </remarks>
+    internal interface IValueFrame : IDisposable
+    {
+        /// <summary>
+        /// Gets the number of value entries in the frame.
+        /// </summary>
+        int EntryCount { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether the frame is active.
+        /// </summary>
+        bool IsActive { get; }
+
+        /// <summary>
+        /// Gets the value store that owns the frame.
+        /// </summary>
+        ValueStore? Owner { get; }
+
+        /// <summary>
+        /// Gets the frame's priority.
+        /// </summary>
+        BindingPriority Priority { get; }
+
+        /// <summary>
+        /// Retreives the frame's value entry with the specified index.
+        /// </summary>
+        IValueEntry GetEntry(int index);
+
+        /// <summary>
+        /// Sets the owner of the value frame.
+        /// </summary>
+        /// <param name="owner">The new owner.</param>
+        void SetOwner(ValueStore? owner);
+
+        /// <summary>
+        /// Tries to retreive the frame's value entry for the specified property.
+        /// </summary>
+        bool TryGetEntry(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? entry);
+    }
+}

+ 44 - 0
src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs

@@ -0,0 +1,44 @@
+using System;
+
+namespace Avalonia.PropertyStore
+{
+    internal class ImmediateValueEntry<T> : IValueEntry<T>, IDisposable
+    {
+        private readonly ImmediateValueFrame _owner;
+        private readonly T _value;
+
+        public ImmediateValueEntry(
+            ImmediateValueFrame owner,
+            StyledPropertyBase<T> property, 
+            T value)
+        {
+            _owner = owner;
+            _value = value;
+            Property = property;
+        }
+
+        public StyledPropertyBase<T> 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;
+    }
+}

+ 60 - 0
src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs

@@ -0,0 +1,60 @@
+using System;
+using Avalonia.Data;
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Holds values in a <see cref="ValueStore"/> set by one of the SetValue or AddBinding
+    /// overloads with non-LocalValue priority.
+    /// </summary>
+    internal class ImmediateValueFrame : ValueFrameBase
+    {
+        public ImmediateValueFrame(BindingPriority priority)
+        {
+            Priority = priority;
+        }
+
+        public override bool IsActive => true;
+        public override BindingPriority Priority { get; }
+
+        public BindingEntry<T> AddBinding<T>(
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source)
+        {
+            var e = new BindingEntry<T>(this, property, source);
+            Add(e);
+            return e;
+        }
+
+        public BindingEntry<T> AddBinding<T>(
+            StyledPropertyBase<T> property,
+            IObservable<T> source)
+        {
+            var e = new BindingEntry<T>(this, property, source);
+            Add(e);
+            return e;
+        }
+
+        public UntypedBindingEntry<T> AddBinding<T>(
+            StyledPropertyBase<T> property,
+            IObservable<object?> source)
+        {
+            var e = new UntypedBindingEntry<T>(this, property, source);
+            Add(e);
+            return e;
+        }
+
+        public IDisposable AddValue<T>(StyledPropertyBase<T> property, T value)
+        {
+            var e = new ImmediateValueEntry<T>(this, property, value);
+            Add(e);
+            return e;
+        }
+
+        public void OnEntryDisposed(IValueEntry value)
+        {
+            Remove(value.Property);
+            Owner?.OnValueEntryRemoved(this, value.Property);
+        }
+    }
+}

+ 34 - 0
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<AvaloniaProperty, EffectiveValue>
+    {
+        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;
+    }
+}

+ 59 - 0
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@@ -0,0 +1,59 @@
+using System;
+using Avalonia.Data;
+
+namespace Avalonia.PropertyStore
+{
+    internal class LocalValueBindingObserver<T> : IObserver<T>,
+        IObserver<BindingValue<T>>,
+        IDisposable
+    {
+        private readonly ValueStore _owner;
+        private IDisposable? _subscription;
+
+        public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase<T> property)
+        {
+            _owner = owner;
+            Property = property;
+        }
+
+        public StyledPropertyBase<T> Property { get;}
+
+        public void Start(IObservable<T> source)
+        {
+            _subscription = source.Subscribe(this);
+        }
+
+        public void Start(IObservable<BindingValue<T>> 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<T> 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);
+        }
+    }
+}

+ 0 - 41
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@@ -1,41 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using Avalonia.Data;
-
-namespace Avalonia.PropertyStore
-{
-    /// <summary>
-    /// Stores a value with local value priority in a <see cref="ValueStore"/> or
-    /// <see cref="PriorityValue{T}"/>.
-    /// </summary>
-    /// <typeparam name="T">The property type.</typeparam>
-    internal class LocalValueEntry<T> : IValue<T>
-    {
-        private T _value;
-
-        public LocalValueEntry(T value) => _value = value;
-        public BindingPriority Priority => BindingPriority.LocalValue;
-        Optional<object?> IValue.GetValue() => new Optional<object?>(_value);
-        
-        public Optional<T> GetValue(BindingPriority maxPriority)
-        {
-            return BindingPriority.LocalValue >= maxPriority ? _value : Optional<T>.Empty;
-        }
-
-        public void SetValue(T value) => _value = value;
-        public void Start() { }
-
-        public void RaiseValueChanged(
-            AvaloniaObject owner,
-            AvaloniaProperty property,
-            Optional<object?> oldValue,
-            Optional<object?> newValue)
-        {
-            owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                owner,
-                (AvaloniaProperty<T>)property,
-                oldValue.Cast<T>(),
-                newValue.Cast<T>(),
-                BindingPriority.LocalValue));
-        }
-    }
-}

+ 61 - 0
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs

@@ -0,0 +1,61 @@
+using System;
+using Avalonia.Data;
+
+namespace Avalonia.PropertyStore
+{
+    internal class LocalValueUntypedBindingObserver<T> : IObserver<object?>,
+        IDisposable
+    {
+        private readonly ValueStore _owner;
+        private IDisposable? _subscription;
+
+        public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase<T> property)
+        {
+            _owner = owner;
+            Property = property;
+        }
+
+        public StyledPropertyBase<T> Property { get; }
+
+        public void Start(IObservable<object?> 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);
+            }
+        }
+    }
+}

+ 61 - 0
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<T>(
+            AvaloniaObject owner,
+            AvaloniaProperty property,
+            BindingValue<T> 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<T>(
+            AvaloniaObject owner,
+            AvaloniaProperty property,
+            BindingValue<T> value)
+        {
+            owner.GetBindingWarningLogger(property, value.Error)?.Log(
+                owner,
+                "Error in binding to {Target}.{Property}: {Message}",
+                owner,
+                property,
+                value.Error!.Message);
+        }
+    }
+}

+ 0 - 326
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -1,326 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.Data;
-
-namespace Avalonia.PropertyStore
-{
-    /// <summary>
-    /// Represents an untyped interface to <see cref="PriorityValue{T}"/>.
-    /// </summary>
-    interface IPriorityValue : IValue
-    {
-        void UpdateEffectiveValue();
-    }
-
-    /// <summary>
-    /// Stores a set of prioritized values and bindings in a <see cref="ValueStore"/>.
-    /// </summary>
-    /// <typeparam name="T">The property type.</typeparam>
-    /// <remarks>
-    /// When more than a single value or binding is applied to a property in an
-    /// <see cref="AvaloniaObject"/>, the entry in the <see cref="ValueStore"/> is converted into
-    /// a <see cref="PriorityValue{T}"/>. This class holds any number of
-    /// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
-    /// they were added) plus a local value.
-    /// </remarks>
-    internal class PriorityValue<T> : IPriorityValue, IValue<T>, IBatchUpdate
-    {
-        private readonly AvaloniaObject _owner;
-        private readonly ValueStore _store;
-        private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
-        private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
-        private Optional<T> _localValue;
-        private Optional<T> _value;
-        private bool _isCalculatingValue;
-        private bool _batchUpdate;
-
-        public PriorityValue(
-            AvaloniaObject owner,
-            StyledPropertyBase<T> 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<T> property,
-            ValueStore store,
-            IPriorityValueEntry<T> 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<T> property,
-            ValueStore sink,
-            LocalValueEntry<T> existing)
-            : this(owner, property, sink)
-        {
-            _value = _localValue = existing.GetValue(BindingPriority.LocalValue);
-            Priority = BindingPriority.LocalValue;
-        }
-
-        public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; private set; } = BindingPriority.Unset;
-        public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
-        Optional<object?> 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<T>(
-                _owner,
-                Property,
-                default,
-                default,
-                BindingPriority.LocalValue));
-        }
-
-        public Optional<T> 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<T>(Property, value, priority, new ValueOwner<T>(this));
-                _entries.Insert(insert, entry);
-                result = entry;
-            }
-
-            UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
-                _owner,
-                Property,
-                default,
-                value,
-                priority));
-
-            return result;
-        }
-
-        public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)
-        {
-            var binding = new BindingEntry<T>(_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<object?> oldValue,
-            Optional<object?> newValue)
-        {
-            owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                owner,
-                (AvaloniaProperty<T>)property,
-                oldValue.Cast<T>(),
-                newValue.Cast<T>(),
-                Priority));
-        }
-
-        public void ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
-        {
-            if (change.Priority == BindingPriority.LocalValue)
-            {
-                _localValue = default;
-            }
-
-            if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
-            {
-                UpdateEffectiveValue(c);
-            }
-        }
-
-        public void Completed(IPriorityValueEntry entry, Optional<T> oldValue)
-        {
-            _entries.Remove((IPriorityValueEntry<T>)entry);
-            UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
-                _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<T>, 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<T>? 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<T>(
-                    _owner,
-                    Property,
-                    old,
-                    value,
-                    Priority));
-            }
-            else if (change is object)
-            {
-                change.MarkNonEffectiveValue();
-                change.SetOldValue(default);
-                _store.ValueChanged(change);
-            }
-        }
-    }
-}

+ 163 - 0
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<T> : IValueEntry<T>,
+        IObserver<object?>,
+        IDisposable
+    {
+        private readonly ValueFrameBase _frame;
+        private readonly IObservable<object?> _source;
+        private IDisposable? _subscription;
+        private bool _hasValue;
+        private T? _value;
+
+        public UntypedBindingEntry(
+            ValueFrameBase frame,
+            StyledPropertyBase<T> property,
+            IObservable<object?> source)
+        {
+            _frame = frame;
+            _source = source;
+            Property = property;
+        }
+
+        public bool HasValue
+        {
+            get
+            {
+                StartIfNecessary();
+                return _hasValue;
+            }
+        }
+
+        public StyledPropertyBase<T> 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<T> 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<T> 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<T>.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();
+        }
+    }
+}

+ 37 - 0
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<T>(
+            StyledPropertyBase<T> 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;
+        }
+    }
+}

+ 54 - 0
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<IValueEntry> _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);
+    }
+}

+ 0 - 45
src/Avalonia.Base/PropertyStore/ValueOwner.cs

@@ -1,45 +0,0 @@
-using Avalonia.Data;
-
-namespace Avalonia.PropertyStore
-{
-    /// <summary>
-    /// Represents a union type of <see cref="ValueStore"/> and <see cref="PriorityValue{T}"/>,
-    /// which are the valid owners of a value store <see cref="IValue"/>.
-    /// </summary>
-    /// <typeparam name="T">The value type.</typeparam>
-    internal readonly struct ValueOwner<T>
-    {
-        private readonly ValueStore? _store;
-        private readonly PriorityValue<T>? _priorityValue;
-
-        public ValueOwner(ValueStore o)
-        {
-            _store = o;
-            _priorityValue = null;
-        }
-
-        public ValueOwner(PriorityValue<T> v)
-        {
-            _store = null;
-            _priorityValue = v;
-        }
-
-        public bool IsValueStore => _store is not null;
-
-        public void Completed(StyledPropertyBase<T> property, IPriorityValueEntry entry, Optional<T> oldValue)
-        {
-            if (_store is not null)
-                _store?.Completed(property, entry, oldValue);
-            else
-                _priorityValue!.Completed(entry, oldValue);
-        }
-
-        public void ValueChanged(AvaloniaPropertyChangedEventArgs<T> e)
-        {
-            if (_store is not null)
-                _store?.ValueChanged(e);
-            else
-                _priorityValue!.ValueChanged(e);
-        }
-    }
-}

+ 948 - 0
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<IValueFrame> _frames = new();
+        private Dictionary<int, IDisposable>? _localValueBindings;
+        private InheritanceFrame? _inheritanceFrame;
+        private Dictionary<AvaloniaProperty, EffectiveValue>? _effectiveValues;
+        private int _frameGeneration;
+        private int _styling;
+
+        public ValueStore(AvaloniaObject owner) => Owner = owner;
+
+        public AvaloniaObject Owner { get; }
+        public IReadOnlyList<IValueFrame> Frames => _frames;
+
+        public void BeginStyling() => ++_styling;
+
+        public void EndStyling()
+        {
+            if (--_styling == 0)
+                ReevaluateEffectiveValues();
+        }
+
+        public void AddFrame(IValueFrame style)
+        {
+            InsertFrame(style);
+            ReevaluateEffectiveValues();
+        }
+
+        public IDisposable AddBinding<T>(
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority)
+        {
+            if (priority == BindingPriority.LocalValue)
+            {
+                var observer = new LocalValueBindingObserver<T>(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<T>(
+            StyledPropertyBase<T> property,
+            IObservable<T> source,
+            BindingPriority priority)
+        {
+            if (priority == BindingPriority.LocalValue)
+            {
+                var observer = new LocalValueBindingObserver<T>(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<T>(
+            StyledPropertyBase<T> property,
+            IObservable<object?> source,
+            BindingPriority priority)
+        {
+            if (priority == BindingPriority.LocalValue)
+            {
+                var observer = new LocalValueUntypedBindingObserver<T>(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<T>(StyledPropertyBase<T> 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<T>)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<T>(StyledPropertyBase<T> property)
+        {
+            if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v))
+                return ((EffectiveValue<T>)v).Value;
+            if (_inheritanceFrame is not null && _inheritanceFrame.TryGetFromThisOrAncestor(property, out v))
+                return ((EffectiveValue<T>)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<T> GetBaseValue<T>(StyledPropertyBase<T> property)
+        {
+            if (TryGetEffectiveValue(property, out var v) &&
+                ((EffectiveValue<T>)v).TryGetBaseValue(out var baseValue))
+            {
+                return baseValue;
+            }
+
+            return default;
+        }
+
+        public void SetInheritanceParent(AvaloniaObject? oldParent, AvaloniaObject? newParent)
+        {
+            var values = DictionaryPool<AvaloniaProperty, OldNewValue>.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<AvaloniaProperty, OldNewValue>.Release(values);
+        }
+
+        public void FrameActivationChanged(IValueFrame frame)
+        {
+            ReevaluateEffectiveValues();
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <returns></returns>
+        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;
+        }
+
+        /// <summary>
+        /// Called by non-LocalValue binding entries to re-evaluate the effective value when the
+        /// binding produces a new value.
+        /// </summary>
+        /// <param name="property">The bound property.</param>
+        /// <param name="priority">The priority of binding which produced a new value.</param>
+        /// <param name="value">The new value.</param>
+        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);
+            }
+        }
+
+        /// <summary>
+        /// Called by non-LocalValue binding entries to re-evaluate the effective value when the
+        /// binding produces a new value.
+        /// </summary>
+        /// <param name="property">The bound property.</param>
+        /// <param name="priority">The priority of binding which produced a new value.</param>
+        /// <param name="value">The new value.</param>
+        public void OnBindingValueChanged<T>(
+            StyledPropertyBase<T> 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);
+            }
+        }
+
+        /// <summary>
+        /// Called by non-LocalValue binding entries to re-evaluate the effective value when the
+        /// binding produces an unset value.
+        /// </summary>
+        /// <param name="property">The bound property.</param>
+        /// <param name="priority">The priority of binding which produced a new value.</param>
+        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);
+            }
+        }
+
+        /// <summary>
+        /// Called by a <see cref="BindingEntry{T}"/> to re-evaluate the effective value when the
+        /// binding completes or terminates on error.
+        /// </summary>
+        /// <param name="property">The previously bound property.</param>
+        /// <param name="frame">The frame which contained the binding.</param>
+        public void OnBindingCompleted(AvaloniaProperty property, IValueFrame frame)
+        {
+            var priority = frame.Priority;
+
+            if (TryGetEffectiveValue(property, out var existing))
+            {
+                if (priority <= existing.Priority)
+                    ReevaluateEffectiveValue(property, existing);
+            }
+        }
+
+        /// <summary>
+        /// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
+        /// changes its value on this value store.
+        /// </summary>
+        /// <param name="property">The property whose value changed.</param>
+        /// <param name="oldValue">The old value of the property.</param>
+        /// <param name="value">The effective value instance.</param>
+        public void OnInheritedEffectiveValueChanged<T>(
+            StyledPropertyBase<T> property, 
+            T oldValue,
+            EffectiveValue<T> 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);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
+        /// is removed from the effective values.
+        /// </summary>
+        /// <param name="property">The property whose value changed.</param>
+        /// <param name="oldValue">The old value of the property.</param>
+        public void OnInheritedEffectiveValueDisposed<T>(StyledPropertyBase<T> 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);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called when a <see cref="LocalValueBindingObserver{T}"/> completes.
+        /// </summary>
+        /// <param name="property">The previously bound property.</param>
+        /// <param name="observer">The observer.</param>
+        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<T>(
+            StyledPropertyBase<T> 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);
+            }
+        }
+
+        /// <summary>
+        /// Called by an <see cref="IValueFrame"/> to re-evaluate the effective value when a value
+        /// is removed.
+        /// </summary>
+        /// <param name="frame">The frame on which the change occurred.</param>
+        /// <param name="property">The property whose value was removed.</param>
+        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);
+            }
+        }
+
+        /// <summary>
+        /// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
+        /// event and notifies inheritance children if necessary .
+        /// </summary>
+        /// <typeparam name="T">The property type.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The property value.</param>
+        /// <param name="priority">The value priority.</param>
+        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);
+        }
+
+        /// <summary>
+        /// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
+        /// event and notifies inheritance children if necessary .
+        /// </summary>
+        /// <typeparam name="T">The property type.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The property value.</param>
+        /// <param name="priority">The value priority.</param>
+        private void AddEffectiveValueAndRaise<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
+        {
+            Debug.Assert(priority < BindingPriority.Inherited);
+            var defaultValue = property.GetDefaultValue(Owner.GetType());
+            var effectiveValue = new EffectiveValue<T>(defaultValue, BindingPriority.Unset);
+            _effectiveValues ??= new();
+            _effectiveValues.Add(property, effectiveValue);
+            effectiveValue.SetAndRaise(this, property, value, priority);
+        }
+
+        /// <summary>
+        /// Evaluates the current value and base value for a property based on the current frames and optionally
+        /// local values. Does not evaluate inherited values.
+        /// </summary>
+        /// <param name="property">The property to evaluation</param>
+        /// <param name="current">The current effective value if the local value is to be considered.</param>
+        /// <param name="value">When the method exits will contain the current value if it exists.</param>
+        /// <param name="priority">When the method exits will contain the current value priority.</param>
+        /// <param name="baseValue">>When the method exits will contain the current base value if it exists.</param>
+        /// <param name="basePriority">When the method exits will contain the current base value priority.</param>
+        /// <returns>
+        /// True if a value was found, otherwise false.
+        /// </returns>
+        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<AvaloniaProperty>? 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<IValueFrame>
+        {
+            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);
+        }
+    }
+}

+ 40 - 90
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<IStyleInstance>? _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<IStyler>()?.ApplyStyles(this);
-                }
-                finally
+                var styler = AvaloniaLocator.Current.GetService<IStyler>();
+
+                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<IStyleInstance>? appliedStyles = _appliedStyles;
+            var styles = new List<IStyleInstance>();
 
-            if (appliedStyles is null)
+            foreach (var frame in GetValueStore().Frames)
             {
-                appliedStyles = Array.Empty<IStyleInstance>();
+                if (frame is IStyleInstance style)
+                    styles.Add(style);
             }
 
-            return new StyleDiagnostics(appliedStyles);
+            return new StyleDiagnostics(styles);
         }
 
         /// <inheritdoc/>
@@ -522,20 +529,8 @@ namespace Avalonia
             return null;
         }
 
-        void IStyleable.StyleApplied(IStyleInstance instance)
-        {
-            instance = instance ?? throw new ArgumentNullException(nameof(instance));
-
-            _appliedStyles ??= new List<IStyleInstance>();
-            _appliedStyles.Add(instance);
-        }
-
         void IStyleable.DetachStyles() => DetachStyles();
 
-        void IStyleable.DetachStyles(IReadOnlyList<IStyle> styles) => DetachStyles(styles);
-
-        void IStyleable.InvalidateStyles() => InvalidateStyles();
-
         void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
         {
             InvalidateStylesOnThisAndDescendents();
@@ -830,56 +825,25 @@ namespace Avalonia
             }
         }
 
-        private void DetachStyles()
+        private void DetachStyles(IReadOnlyList<StyleBase>? 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<IStyle> 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<IStyle> styles)
+        private void DetachStylesFromThisAndDescendents(IReadOnlyList<StyleBase> styles)
         {
             DetachStyles(styles);
 
@@ -927,38 +891,24 @@ namespace Avalonia
             }
         }
 
-        private static IReadOnlyList<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles)
+        private static IReadOnlyList<StyleBase> RecurseStyles(IReadOnlyList<IStyle> styles)
         {
-            var count = styles.Count;
-            List<IStyle>? 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<IStyle>(styles);
-                    }
-
-                    RecurseStyles(style.Children, result);
-                }
-            }
-
-            return result ?? styles;
+            var result = new List<StyleBase>();
+            RecurseStyles(styles, result);
+            return result;
         }
 
-        private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<IStyle> result)
+        private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<StyleBase> 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<IStyle> children)
+                    RecurseStyles(children, result);
             }
         }
     }

+ 39 - 45
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
         /// <inheritdoc/>
         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<TValue>(GetDefaultValue(o.GetType()), BindingPriority.Unset);
+        }
+
         /// <inheritdoc/>
         internal override void RouteClearValue(AvaloniaObject o)
         {
@@ -182,34 +199,44 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority)
+        internal override object? RouteGetBaseValue(AvaloniaObject o)
         {
-            var value = o.GetBaseValue<TValue>(this, maxPriority);
+            var value = o.GetBaseValue<TValue>(this);
             return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue;
         }
 
         /// <inheritdoc/>
         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<TValue>(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<TValue>(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<object?> source,
+            BindingPriority priority)
+        {
+            return target.Bind<TValue>(this, source, priority);
         }
 
         /// <inheritdoc/>
@@ -222,39 +249,6 @@ namespace Avalonia
             return o.Bind<TValue>(this, adapter, priority);
         }
 
-        /// <inheritdoc/>
-        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<TValue>(
-                    target,
-                    this,
-                    binding);
-            }
-            else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType))
-            {
-                return new PropertySetterTemplateInstance<TValue>(
-                    target,
-                    this,
-                    template);
-            }
-            else
-            {
-                return new PropertySetterInstance<TValue>(
-                    target,
-                    this,
-                    (TValue)value!);
-            }
-        }
-
         private object? GetDefaultBoxedValue(Type type)
         {
             _ = type ?? throw new ArgumentNullException(nameof(type));

+ 17 - 0
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<IStyleActivator>();

+ 5 - 0
src/Avalonia.Base/Styling/Activators/IStyleActivator.cs

@@ -18,6 +18,11 @@ namespace Avalonia.Styling.Activators
     [Unstable]
     public interface IStyleActivator : IDisposable
     {
+        /// <summary>
+        /// Gets a value indicating whether the style is activated.
+        /// </summary>
+        bool IsActive { get; }
+
         /// <summary>
         /// Subscribes to the activator.
         /// </summary>

+ 1 - 0
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);

+ 4 - 4
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;
     }
 }

+ 17 - 0
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<IStyleActivator>();

+ 10 - 1
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<object?>.OnCompleted() { }
         void IObserver<object?>.OnError(Exception error) { }
-        void IObserver<object?>.OnNext(object? value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value));
+        void IObserver<object?>.OnNext(object? value) => PublishNext(IsActive);
     }
 }

+ 2 - 2
src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs

@@ -1,5 +1,3 @@
-#nullable enable
-
 namespace Avalonia.Styling.Activators
 {
     /// <summary>
@@ -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)

+ 4 - 4
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<string> classes, IList<string> 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);
     }
 }

+ 1 - 0
src/Avalonia.Base/Styling/ControlTheme.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.PropertyStore;
 
 namespace Avalonia.Styling
 {

+ 6 - 0
src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs

@@ -0,0 +1,6 @@
+namespace Avalonia.Styling
+{
+    internal class DirectPropertySetterBindingInstance : ISetterInstance
+    {
+    }
+}

+ 12 - 0
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
+    {
+    }
+}

+ 3 - 3
src/Avalonia.Base/Styling/ISetter.cs

@@ -12,13 +12,13 @@ namespace Avalonia.Styling
         /// <summary>
         /// Instances a setter on a control.
         /// </summary>
+        /// <param name="styleInstance">The style which contains the setter.</param>
         /// <param name="target">The control.</param>
         /// <returns>An <see cref="ISetterInstance"/>.</returns>
         /// <remarks>
         /// This method should return an <see cref="ISetterInstance"/> which can be used to apply
-        /// the setter to the specified control. Note that it should not apply the setter value 
-        /// until <see cref="ISetterInstance.Start(bool)"/> is called.
+        /// the setter to the specified control.
         /// </remarks>
-        ISetterInstance Instance(IStyleable target);
+        ISetterInstance Instance(IStyleInstance styleInstance, IStyleable target);
     }
 }

+ 3 - 31
src/Avalonia.Base/Styling/ISetterInstance.cs

@@ -1,40 +1,12 @@
-using System;
-using Avalonia.Metadata;
+using Avalonia.Metadata;
 
 namespace Avalonia.Styling
 {
     /// <summary>
-    /// Represents a setter that has been instanced on a control.
+    /// Represents an <see cref="ISetter"/> that has been instanced on a control.
     /// </summary>
     [Unstable]
-    public interface ISetterInstance : IDisposable
+    public interface ISetterInstance
     {
-        /// <summary>
-        /// Starts the setter instance.
-        /// </summary>
-        /// <param name="hasActivator">Whether the parent style has an activator.</param>
-        /// <remarks>
-        /// If <paramref name="hasActivator"/> is false then the setter should be immediately
-        /// applied and <see cref="Activate"/> and <see cref="Deactivate"/> should not be called.
-        /// If true, then bindings etc should be initiated but not produce a value until
-        /// <see cref="Activate"/> called.
-        /// </remarks>
-        public void Start(bool hasActivator);
-
-        /// <summary>
-        /// Activates the setter.
-        /// </summary>
-        /// <remarks>
-        /// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
-        /// </remarks>
-        public void Activate();
-
-        /// <summary>
-        /// Deactivates the setter.
-        /// </summary>
-        /// <remarks>
-        /// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
-        /// </remarks>
-        public void Deactivate();
     }
 }

+ 8 - 11
src/Avalonia.Base/Styling/IStyleInstance.cs

@@ -1,13 +1,12 @@
-using System;
-using Avalonia.Metadata;
+using Avalonia.Metadata;
 
 namespace Avalonia.Styling
 {
     /// <summary>
-    /// Represents a style that has been instanced on a control.
+    /// Represents a <see cref="Style"/> that has been instanced on a control.
     /// </summary>
     [Unstable]
-    public interface IStyleInstance : IDisposable
+    public interface IStyleInstance
     {
         /// <summary>
         /// Gets the source style.
@@ -15,18 +14,16 @@ namespace Avalonia.Styling
         IStyle Source { get; }
 
         /// <summary>
-        /// Gets a value indicating whether this style has an activator.
+        /// Gets a value indicating whether this style instance has an activator.
         /// </summary>
+        /// <remarks>
+        /// A style instance without an activator will always be active.
+        /// </remarks>
         bool HasActivator { get; }
-        
+
         /// <summary>
         /// Gets a value indicating whether this style is active.
         /// </summary>
         bool IsActive { get; }
-
-        /// <summary>
-        /// Instructs the style to start acting upon the control.
-        /// </summary>
-        void Start();
     }
 }

+ 0 - 20
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
         /// </summary>
         ControlTheme? GetEffectiveTheme();
 
-        /// <summary>
-        /// Notifies the element that a style has been applied.
-        /// </summary>
-        /// <param name="instance">The style instance.</param>
-        void StyleApplied(IStyleInstance instance);
-
-        /// <summary>
-        /// Detaches all styles applied to the element.
-        /// </summary>
         void DetachStyles();
-
-        /// <summary>
-        /// Detaches a collection of styles, if applied to the element.
-        /// </summary>
-        void DetachStyles(IReadOnlyList<IStyle> styles);
-
-        /// <summary>
-        /// Detaches all styles from the element and queues a restyle.
-        /// </summary>
-        void InvalidateStyles();
     }
 }

+ 19 - 178
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
 {
-    /// <summary>
-    /// A <see cref="Setter"/> which has been instanced on a control and has an
-    /// <see cref="IBinding"/> as its value.
-    /// </summary>
-    /// <typeparam name="T">The target property type.</typeparam>
-    internal class PropertySetterBindingInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
-        ISubject<BindingValue<T>>,
-        ISetterInstance
+    internal class PropertySetterBindingInstance : BindingEntry, ISetterInstance
     {
-        private readonly IStyleable _target;
-        private readonly StyledPropertyBase<T>? _styledProperty;
-        private readonly DirectPropertyBase<T>? _directProperty;
-        private readonly InstancedBinding? _binding;
-        private readonly Inner _inner;
-        private BindingValue<T> _value;
-        private IDisposable? _subscription;
-        private IDisposable? _subscriptionTwoWay;
-        private IDisposable? _innerSubscription;
-        private bool _isActive;
-
-        public PropertySetterBindingInstance(
-            IStyleable target,
-            StyledPropertyBase<T> 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<T> 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<object?> 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<BindingValue<T>>.OnCompleted()
-        {
-            // This is the observable coming from the target control. It should not complete.
-        }
-
-        void IObserver<BindingValue<T>>.OnError(Exception error)
-        {
-            // This is the observable coming from the target control. It should not error.
-        }
-
-        void IObserver<BindingValue<T>>.OnNext(BindingValue<T> 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<object?> 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<T>.FromUntyped(value);
-
-            if (_isActive)
-            {
-                PublishNext();
-            }
-        }
-
-        private class Inner : IObserver<object?>
+        public override void Unsubscribe()
         {
-            private readonly PropertySetterBindingInstance<T> _owner;
-            public Inner(PropertySetterBindingInstance<T> 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();
         }
     }
 }

+ 14 - 107
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
 {
-    /// <summary>
-    /// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
-    /// evaluated.
-    /// </summary>
-    /// <typeparam name="T">The target property type.</typeparam>
-    internal class PropertySetterTemplateInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
-        ISetterInstance
+    internal class PropertySetterTemplateInstance : IValueEntry, ISetterInstance
     {
-        private readonly IStyleable _target;
-        private readonly StyledPropertyBase<T>? _styledProperty;
-        private readonly DirectPropertyBase<T>? _directProperty;
         private readonly ITemplate _template;
-        private BindingValue<T> _value;
-        private IDisposable? _subscription;
-        private bool _isActive;
-
-        public PropertySetterTemplateInstance(
-            IStyleable target,
-            StyledPropertyBase<T> property,
-            ITemplate template)
-        {
-            _target = target;
-            _styledProperty = property;
-            _template = template;
-        }
+        private object? _value;
 
-        public PropertySetterTemplateInstance(
-            IStyleable target,
-            DirectPropertyBase<T> 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() { }
     }
 }

+ 71 - 9
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 <see cref="Setter"/> is used to set a <see cref="AvaloniaProperty"/> value on a
     /// <see cref="AvaloniaObject"/> depending on a condition.
     /// </remarks>
-    public class Setter : ISetter, IAnimationSetter
+    public class Setter : ISetter, IValueEntry, ISetterInstance, IAnimationSetter
     {
         private object? _value;
+        private DirectPropertySetterInstance? _direct;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Setter"/> class.
@@ -30,7 +30,7 @@ namespace Avalonia.Styling
         /// </summary>
         /// <param name="property">The property to set.</param>
         /// <param name="value">The property value.</param>
-        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();
         }
     }
 }

+ 26 - 0
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
     /// </summary>
     public class Style : StyleBase
     {
+        private bool? _inControlTheme;
         private Selector? _selector;
 
         /// <summary>
@@ -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)

+ 18 - 4
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)

+ 40 - 103
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
 {
     /// <summary>
-    /// A <see cref="Style"/> which has been instanced on a control.
+    /// Stores state for a <see cref="Style"/> that has been instanced on a control.
     /// </summary>
-    internal sealed class StyleInstance : IStyleInstance, IStyleActivatorSink
+    /// <remarks>
+    /// <see cref="StyleInstance"/> implements the <see cref="IValueFrame"/> interface meaning that
+    /// it is injected directly into the value store of an <see cref="AvaloniaObject"/>. 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.
+    /// </remarks>
+    internal class StyleInstance : ValueFrameBase, IStyleInstance, IStyleActivatorSink, IDisposable
     {
-        private readonly ISetterInstance[]? _setters;
-        private readonly IDisposable[]? _animations;
         private readonly IStyleActivator? _activator;
-        private readonly Subject<bool>? _animationTrigger;
+        private List<ISetterInstance>? _setters;
+        private bool _isActivatorInitializing;
+        private bool _isActivatorSubscribed;
 
-        public StyleInstance(
-            IStyle source,
-            IStyleable target,
-            IReadOnlyList<ISetter>? setters,
-            IReadOnlyList<IAnimation>? 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<bool>();
-
-                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);
     }
 }

+ 8 - 1
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)

+ 0 - 507
src/Avalonia.Base/ValueStore.cs

@@ -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
-{
-    /// <summary>
-    /// Stores styled property values for an <see cref="AvaloniaObject"/>.
-    /// </summary>
-    /// <remarks>
-    /// At its core this class consists of an <see cref="AvaloniaProperty"/> to 
-    /// <see cref="IValue"/> mapping which holds the current values for each set property. This
-    /// <see cref="IValue"/> can be in one of 4 states:
-    /// 
-    /// - For a single local value it will be an instance of <see cref="LocalValueEntry{T}"/>.
-    /// - For a single value of a priority other than LocalValue it will be an instance of
-    ///   <see cref="ConstantValueEntry{T}"/>`
-    /// - For a single binding it will be an instance of <see cref="BindingEntry{T}"/>
-    /// - For all other cases it will be an instance of <see cref="PriorityValue{T}"/>
-    /// </remarks>
-    internal class ValueStore
-    {
-        private readonly AvaloniaObject _owner;
-        private readonly AvaloniaPropertyValueStore<IValue> _values;
-        private BatchUpdate? _batchUpdate;
-
-        public ValueStore(AvaloniaObject owner)
-        {
-            _owner = owner;
-            _values = new AvaloniaPropertyValueStore<IValue>();
-        }
-
-        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<T>(
-            StyledPropertyBase<T> property,
-            BindingPriority maxPriority,
-            out T value)
-        {
-            if (TryGetValue(property, out var slot))
-            {
-                var v = ((IValue<T>)slot).GetValue(maxPriority);
-
-                if (v.HasValue)
-                {
-                    value = v.Value;
-                    return true;
-                }
-            }
-
-            value = default!;
-            return false;
-        }
-
-        public IDisposable? SetValue<T>(StyledPropertyBase<T> 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<T>(_owner, property, this);
-                AddValue(property, entry);
-                result = entry.SetValue(value, priority);
-            }
-            else
-            {
-                if (priority == BindingPriority.LocalValue)
-                {
-                    AddValue(property, new LocalValueEntry<T>(value));
-                    NotifyValueChanged<T>(property, default, value, priority);
-                }
-                else
-                {
-                    var entry = new ConstantValueEntry<T>(property, value, priority, new(this));
-                    AddValue(property, entry);
-                    NotifyValueChanged<T>(property, default, value, priority);
-                    result = entry;
-                }
-            }
-
-            return result;
-        }
-
-        public IDisposable AddBinding<T>(
-            StyledPropertyBase<T> property,
-            IObservable<BindingValue<T>> 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<T>(_owner, property, this);
-                var binding = entry.AddBinding(source, priority);
-                AddValue(property, entry);
-                return binding;
-            }
-            else
-            {
-                var entry = new BindingEntry<T>(_owner, property, source, priority, new(this));
-                AddValue(property, entry);
-                return entry;
-            }
-        }
-
-        public void ClearLocalValue<T>(StyledPropertyBase<T> property)
-        {
-            if (TryGetValue(property, out var slot))
-            {
-                if (slot is PriorityValue<T> p)
-                {
-                    p.ClearLocalValue();
-                }
-                else if (slot.Priority == BindingPriority.LocalValue)
-                {
-                    var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ?
-                        new Optional<T>(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<T>(property, Optional<T>.Empty, BindingPriority.Unset, new(this));
-                        _values.SetValue(property, sentinel);
-                    }
-
-                    NotifyValueChanged<T>(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<T>(AvaloniaPropertyChangedEventArgs<T> change)
-        {
-            if (_batchUpdate is object)
-            {
-                if (change.IsEffectiveValueChange)
-                {
-                    NotifyValueChanged<T>(change.Property, change.OldValue, change.NewValue, change.Priority);
-                }
-            }
-            else
-            {
-                _owner.ValueChanged(change);
-            }
-        }
-
-        public void Completed<T>(
-            StyledPropertyBase<T> property,
-            IPriorityValueEntry entry,
-            Optional<T> 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<T>(
-            object slot,
-            StyledPropertyBase<T> property,
-            T value,
-            BindingPriority priority)
-        {
-            IDisposable? result = null;
-
-            if (slot is IPriorityValueEntry<T> e)
-            {
-                var priorityValue = new PriorityValue<T>(_owner, property, this, e);
-                _values.SetValue(property, priorityValue);
-                result = priorityValue.SetValue(value, priority);
-            }
-            else if (slot is PriorityValue<T> p)
-            {
-                result = p.SetValue(value, priority);
-            }
-            else if (slot is LocalValueEntry<T> l)
-            {
-                if (priority == BindingPriority.LocalValue)
-                {
-                    var old = l.GetValue(BindingPriority.LocalValue);
-                    l.SetValue(value);
-                    NotifyValueChanged<T>(property, old, value, priority);
-                }
-                else
-                {
-                    var priorityValue = new PriorityValue<T>(_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<T>(
-            object slot,
-            StyledPropertyBase<T> property,
-            IObservable<BindingValue<T>> source,
-            BindingPriority priority)
-        {
-            PriorityValue<T> priorityValue;
-
-            if (slot is IPriorityValueEntry<T> e)
-            {
-                priorityValue = new PriorityValue<T>(_owner, property, this, e);
-
-                if (IsBatchUpdating())
-                {
-                    priorityValue.BeginBatchUpdate();
-                }
-            }
-            else if (slot is PriorityValue<T> p)
-            {
-                priorityValue = p;
-            }
-            else if (slot is LocalValueEntry<T> l)
-            {
-                priorityValue = new PriorityValue<T>(_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<T>(
-            AvaloniaProperty<T> property,
-            Optional<T> oldValue,
-            BindingValue<T> newValue,
-            BindingPriority priority)
-        {
-            if (_batchUpdate is null)
-            {
-                _owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                    _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<Notification>? _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<T>.
-                            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<object?> oldValue)
-            {
-                _notifications ??= new List<Notification>();
-
-                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<object?> oldValue;
-            }
-        }
-    }
-}

+ 15 - 18
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);
         }
 
         /// <summary>

+ 5 - 5
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();
+            //}
         }
 
         /// <summary>

+ 15 - 7
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()

+ 0 - 695
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@@ -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<string>();
-
-            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<string>("foo");
-            var raised = new List<string>();
-
-            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<string>("foo");
-            var raised = new List<string>();
-
-            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<string>("foo");
-            var raised = new List<string>();
-
-            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<AvaloniaPropertyChangedEventArgs>();
-
-            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<AvaloniaPropertyChangedEventArgs>();
-
-            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<AvaloniaPropertyChangedEventArgs>();
-
-            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<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("baz");
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("foo");
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("foo");
-            var observable2 = new TestObservable<string>("qux");
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("foo");
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("bar");
-            var observable2 = new TestObservable<string>("baz");
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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>(Orientation.Horizontal);
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("foo");
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("foo");
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("foo");
-            var observable2 = new TestObservable<string>("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<string>("foo");
-            var observable2 = new TestObservable<string>("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<string>("foo");
-            var observable2 = new TestObservable<string>("bar");
-            var raised = new List<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("foo");
-            var observable2 = new TestObservable<string>("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<string>("foo");
-            var observable2 = new TestObservable<string>("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<string>("foo");
-            var observable2 = new TestObservable<string>("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<AvaloniaPropertyChangedEventArgs>();
-
-            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<AvaloniaPropertyChangedEventArgs>();
-
-            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<AvaloniaPropertyChangedEventArgs>();
-
-            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<AvaloniaPropertyChangedEventArgs>();
-
-            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<string>("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<AvaloniaPropertyChangedEventArgs>();
-            var observable1 = new TestObservable<string>("foo");
-            var observable2 = new TestObservable<string>("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<AvaloniaPropertyChangedEventArgs>();
-
-            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<string> FooProperty =
-                AvaloniaProperty.Register<TestClass, string>(nameof(Foo));
-
-            public static readonly StyledProperty<string> BarProperty =
-                AvaloniaProperty.Register<TestClass, string>(nameof(Bar));
-
-            public static readonly StyledProperty<Orientation> BazProperty =
-                AvaloniaProperty.Register<TestClass, Orientation>(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<T> : ObservableBase<BindingValue<T>>
-        {
-            private readonly T _value;
-            private IObserver<BindingValue<T>> _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<BindingValue<T>> observer)
-            {
-                ++SubscribeCount;
-                _observer = observer;
-                observer.OnNext(_value);
-                return Disposable.Empty;
-            }
-        }
-    }
-}

+ 205 - 167
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<BindingValue<string>>("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<BindingValue<string>>();
-            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<string>();
+            var source = new Subject<BindingValue<string>>();
             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<string>();
+            var source = new Subject<BindingValue<string>>();
             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<string>("bar");
+            var source = new BehaviorSubject<BindingValue<string>>("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<BindingValue<string>>("foo");
+            var source2 = new BehaviorSubject<BindingValue<string>>("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<BindingValue<string>>("foo");
+            var source2 = new BehaviorSubject<BindingValue<string>>("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<string>();
+            var source = new Subject<BindingValue<string>>();
             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<string>("bar"), BindingPriority.Style);
+            target.Bind(property, new BehaviorSubject<BindingValue<string>>("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<string>();
-            var source2 = new Subject<string>();
+            var source1 = new Subject<BindingValue<string>>();
+            var source2 = new Subject<BindingValue<string>>();
 
             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<string>();
-            var source2 = new Subject<string>();
+            var source1 = new Subject<BindingValue<string>>();
+            var source2 = new Subject<BindingValue<string>>();
 
             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<string>();
-            var source2 = new Subject<string>();
+            var source1 = new Subject<BindingValue<string>>();
+            var source2 = new Subject<BindingValue<string>>();
 
             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<object?>(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<object>();
@@ -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<object>();
+
+            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<object>();
+
+            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<object>();
+
+            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<string>().StartWith("foo"));
+            target.Bind(Class2.BarProperty, Observable.Never<BindingValue<string>>().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<string>();
+            var source = scheduler.CreateColdObservable<BindingValue<string>>();
             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<string>();
+            var binding = new Subject<BindingValue<string>>();
 
             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<object?>();
+            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<object?>();
+            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<BindingValue<string>>("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<string>();
+            var source = new Subject<BindingValue<string>>();
             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);
-        }
-
         /// <summary>
         /// Returns an observable that returns a single value but does not complete.
         /// </summary>
         /// <typeparam name="T">The type of the observable.</typeparam>
         /// <param name="value">The value.</param>
         /// <returns>The observable.</returns>
-        private IObservable<T> Single<T>(T value)
+        private IObservable<BindingValue<T>> Single<T>(T value)
         {
-            return Observable.Never<T>().StartWith(value);
+            return Observable.Never<BindingValue<T>>().StartWith(value);
         }
 
         private class Class1 : AvaloniaObject
@@ -918,56 +1022,6 @@ namespace Avalonia.Base.UnitTests
                 AvaloniaProperty.Register<Class2, string>("Bar", "bardefault");
         }
 
-        private class Class3 : AvaloniaObject 
-        {
-            static Class3()
-            {
-                MinimumProperty.Changed.Subscribe(x => OnMinimumChanged(x));
-            }
-
-            private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs<double> e)
-            {
-                if (e.Sender is Class3 s)
-                {
-                    s.SetValue(ValueProperty, MathUtilities.Clamp(s.Value, e.NewValue.Value, double.PositiveInfinity));
-                }
-            }
-
-            /// <summary>
-            /// Defines the <see cref="Value"/> property.
-            /// </summary>
-            public static readonly StyledProperty<double> ValueProperty =
-                AvaloniaProperty.Register<Class3, double>(nameof(Value), 0);
-
-            /// <summary>
-            /// Gets or sets the Value property
-            /// </summary>
-            public double Value
-            {
-                get { return GetValue(ValueProperty); }
-                set { SetValue(ValueProperty, value); }
-            }
-
-
-            /// <summary>
-            /// Defines the <see cref="Minimum"/> property.
-            /// </summary>
-            public static readonly StyledProperty<double> MinimumProperty =
-                AvaloniaProperty.Register<Class3, double>(nameof(Minimum), 0);
-
-            /// <summary>
-            /// Gets or sets the minimum property
-            /// </summary>
-            public double Minimum
-            {
-                get { return GetValue(MinimumProperty); }
-                set { SetValue(MinimumProperty, value); }
-            }
-
-
-        }
-
-
         private class TestOneTimeBinding : IBinding
         {
             private IObservable<object> _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; }
         }
     }
 }

+ 10 - 21
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<string>("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<string>("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

+ 116 - 4
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<string>());
+                Assert.Equal("newvalue", e.GetNewValue<string>());
+                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<string>());
+                Assert.Equal("newvalue", e.GetNewValue<string>());
+                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<string> FooProperty =

+ 5 - 46
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<BindingValue<string>>();
-            var animation = new Subject<BindingValue<string>>();
-            var templatedParent = new Subject<BindingValue<string>>();
-
-            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<AvaloniaPropertyChangedEventArgs<string>>();
-
-            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<string>)change;
-                var result = new AvaloniaPropertyChangedEventArgs<string>(
+                return new AvaloniaPropertyChangedEventArgs<string>(
                     change.Sender,
                     e.Property,
                     e.OldValue,
                     e.NewValue,
-                    change.Priority);
-
-                if (!change.IsEffectiveValueChange)
-                {
-                    result.MarkNonEffectiveValue();
-                }
-
-                return result;
+                    change.Priority,
+                    change.IsEffectiveValueChange);
             }
         }
     }

+ 27 - 1
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<int>();
@@ -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<int>();
+
+            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<int>();
+
+            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()
         {

+ 10 - 6
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<BindingValue<object>> source,
+                IObservable<object> source,
                 BindingPriority priority)
             {
                 throw new NotImplementedException();
             }
 
-            internal override void RouteClearValue(AvaloniaObject o)
+            internal override IDisposable RouteBind(
+                AvaloniaObject o,
+                IObservable<BindingValue<object>> 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();
             }

+ 0 - 314
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@@ -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<string> TestProperty = new StyledProperty<string>(
-            "Test",
-            typeof(PriorityValueTests),
-            new StyledPropertyMetadata<string>());
-
-        [Fact]
-        public void Constructor_Should_Set_Value_Based_On_Initial_Entry()
-        {
-            var target = new PriorityValue<string>(
-                Owner,
-                TestProperty,
-                ValueStore,
-                new ConstantValueEntry<string>(
-                    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<string>(
-                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<string>(
-                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<string>(
-                Owner,
-                TestProperty,
-                ValueStore);
-
-            target.SetValue("1", BindingPriority.Style);
-            target.SetValue("2", BindingPriority.Animation);
-
-            var result = target.Entries
-                .OfType<ConstantValueEntry<string>>()
-                .Select(x => x.GetValue().Value)
-                .ToList();
-
-            Assert.Equal(new[] { "1", "2" }, result);
-        }
-
-        [Fact]
-        public void Priority_Should_Be_Set()
-        {
-            var target = new PriorityValue<string>(
-                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<string>(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<BindingEntry<string>>()
-                .Select(x => x.Source)
-                .OfType<Source>()
-                .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<string>(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<BindingEntry<string>>()
-                .Select(x => x.Source)
-                .OfType<Source>()
-                .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<string>(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<BindingEntry<string>>()
-                .Select(x => x.Source)
-                .OfType<Source>()
-                .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<string>(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<BindingEntry<string>>()
-                .Select(x => x.Source)
-                .OfType<Source>()
-                .Select(x => x.Id)
-                .ToList();
-
-            Assert.Equal(new[] { "2", "3", "1" }, result);
-        }
-
-        [Fact]
-        public void Competed_Binding_Should_Be_Removed()
-        {
-            var target = new PriorityValue<string>(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<BindingEntry<string>>()
-                .Select(x => x.Source)
-                .OfType<Source>()
-                .Select(x => x.Id)
-                .ToList();
-
-            Assert.Equal(new[] { "2", "1" }, result);
-        }
-
-        [Fact]
-        public void Value_Should_Come_From_Last_Entry()
-        {
-            var target = new PriorityValue<string>(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<string>(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<string>(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<string>(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<string>(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<string>(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<BindingValue<string>>
-        {
-            private IObserver<BindingValue<string>> _observer;
-
-            public Source(string id) => Id = id;
-            public string Id { get; }
-
-            public IDisposable Subscribe(IObserver<BindingValue<string>> observer)
-            {
-                _observer = observer;
-                observer.OnNext(Id);
-                return Disposable.Empty;
-            }
-
-            public void OnCompleted() => _observer.OnCompleted();
-        }
-    }
-}

+ 126 - 0
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<string>("bar");
+            var result = new List<PropertyChange>();
+            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<string>("bar");
+            var result = new List<PropertyChange>();
+            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<PropertyChange>();
+            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<string> FooProperty =
+                AvaloniaProperty.Register<Class1, string>("Foo", "foodefault");
+
+            public static readonly StyledProperty<string> BarProperty =
+                AvaloniaProperty.Register<Class1, string>("Bar", "bardefault", true);
+        }
+
+        private record PropertyChange(
+            AvaloniaProperty Property,
+            object? OldValue,
+            object? NewValue);
+    }
+}

+ 249 - 70
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<object>("foo");
             var descriptor = InstancedBinding.OneWay(subject);
-            var binding = Mock.Of<IBinding>(x => x.Initiate(control, TextBlock.TextProperty, null, false) == descriptor);
+            var binding = Mock.Of<IBinding>(x => x.Initiate(control, TextBlock.TagProperty, null, false) == descriptor);
             var style = Mock.Of<IStyle>();
-            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<IStyle>();
             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<IStyle>();
             var setter = new Setter(Decorator.ChildProperty, template);
 
-            setter.Instance(control).Start(false);
+            Apply(setter, control);
 
             Assert.IsType<Canvas>(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<TextBlock>())
+            {
+                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<object?>("foo");
+            var style = new Style(x => x.Is<TextBlock>())
+            {
+                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<object?>("foo");
+            var style = new Style(x => x.Is<TextBlock>().Class("foo"))
+            {
+                Setters =
+                {
+                    new Setter(TextBlock.TextProperty, source.ToBinding()),
+                }
+            };
+
+            Assert.Throws<InvalidOperationException>(() => 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<TextBlock>().Class("foo"))
+            {
+                Setters =
+                {
+                    new Setter(TextBlock.TextProperty, "foo"),
+                }
+            };
+
+            Assert.Throws<InvalidOperationException>(() => Apply(style, control));
+        }
+
         [Fact]
         public void Does_Not_Call_Converter_ConvertBack_On_OneWay_Binding()
         {
-            var control = new Decorator { Name = "foo" };
-            var style = Mock.Of<IStyle>();
+            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<Decorator>().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<Border>())
+            {
+                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<Border>().Class("foo"))
+            {
+                Setters =
+                {
+                    new Setter(Control.TagProperty, "foo"),
+                },
+            };
+            var activator = new Subject<bool>();
+            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<Border>())
+            {
+                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<Border>().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<TextBox>())
+            {
+                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<Border>())
+            {
+                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<Control>())
+            {
+                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

+ 58 - 34
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<Class1>(() =>
-            {
-                ++instantiationCount;
-                return new Class1();
-            });
-
-            Styles styles = new Styles
-            {
-                new Style(x => x.OfType<Class1>())
-                {
-                    Setters =
-                    {
-                        new Setter(Class1.ChildProperty, template),
-                    },
-                },
-
-                new Style(x => x.OfType<Class1>())
-                {
-                    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<Class1>(() =>
+            ////{
+            ////    ++instantiationCount;
+            ////    return new Class1();
+            ////});
+
+            ////Styles styles = new Styles
+            ////{
+            ////    new Style(x => x.OfType<Class1>())
+            ////    {
+            ////        Setters =
+            ////        {
+            ////            new Setter(Class1.ChildProperty, template),
+            ////        },
+            ////    },
+
+            ////    new Style(x => x.OfType<Class1>())
+            ////    {
+            ////        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<Class1>().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()
         {

+ 24 - 43
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<IStyler>(new Styler());
+
                 root.Child = child;
 
                 Assert.Throws<InvalidOperationException>(() => 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<IStyleInstance>();
-                ((IStyleable)child).StyleApplied(styleInstance.Object);
-
-                root.Child = null;
+                Styles =
+                {
+                    new Style(x => x.OfType<Border>())
+                    {
+                        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",

+ 1 - 0
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@@ -5,6 +5,7 @@
     <OutputType>Exe</OutputType>
     <IsPackable>false</IsPackable>
   </PropertyGroup>
+  <Import Project="..\..\build\SharedVersion.props" />
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />

+ 0 - 147
tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs

@@ -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<Style> _styles = new();
-
-        public ControlTheme_Apply()
-        {
-            RuntimeHelpers.RunClassConstructor(typeof(TestControl).TypeHandle);
-
-            _theme = CreateControlTheme(Brushes.Red);
-            _otherTheme = CreateControlTheme(Brushes.Orange);
-
-            for (var i = 0; i < 100; ++i)
-            {
-                _styles.Add(new Style(x => x.OfType<TestControl>())
-                {
-                    Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Yellow) }
-                });
-            }
-        }
-
-        [Benchmark]
-        public void Apply_Control_Theme()
-        {
-            var target = new TestControl();
-
-            target.BeginBatchUpdate();
-
-            _theme.TryAttach(target, null);
-            target.ApplyTemplate();
-            _theme.TryAttach(target.VisualChild, null);
-
-            target.EndBatchUpdate();
-        }
-
-
-        [Benchmark]
-        public void Apply_Remove_Control_Theme()
-        {
-            var target = new TestControl();
-
-            target.BeginBatchUpdate();
-
-            _theme.TryAttach(target, null);
-            target.ApplyTemplate();
-            _theme.TryAttach(target.VisualChild, null);
-
-            target.EndBatchUpdate();
-
-            // Switching to another theme will cause the current theme to be removed but won't
-            // immediately apply the new theme, so for the benefit of the benchmark it has the
-            // effect of simply removing the theme.
-            target.Theme = _otherTheme;
-        }
-
-        [Benchmark]
-        public void Apply_Control_Theme_With_Styles()
-        {
-            var target = new TestControl();
-
-            target.BeginBatchUpdate();
-
-            _theme.TryAttach(target, null);
-            target.ApplyTemplate();
-            _theme.TryAttach(target.VisualChild, null);
-
-            foreach (var style in _styles)
-                style.TryAttach(target, null);
-
-            target.EndBatchUpdate();
-        }
-
-        [Benchmark]
-        public void Apply_Remove_Control_Theme_With_Styles()
-        {
-            var target = new TestControl();
-
-            target.BeginBatchUpdate();
-
-            _theme.TryAttach(target, null);
-            target.ApplyTemplate();
-            _theme.TryAttach(target.VisualChild, null);
-
-            foreach (var style in _styles)
-                style.TryAttach(target, null);
-
-            target.EndBatchUpdate();
-
-            // Switching to another theme will cause the current theme to be removed but won't
-            // immediately apply the new theme, so for the benefit of the benchmark it has the
-            // effect of simply removing the theme.
-            target.Theme = _otherTheme;
-        }
-
-        private static ControlTheme CreateControlTheme(IBrush background)
-        {
-            return new ControlTheme(typeof(TestControl))
-            {
-                Setters =
-                {
-                    new Setter(TestControl.BackgroundProperty, Brushes.Transparent),
-                    new Setter(TestControl.TemplateProperty, new FuncControlTemplate<TestControl>((_, x) =>
-                        new Border())),
-                },
-                Children =
-                {
-                    new Style(x => x.Nesting().Template().OfType<Border>())
-                    {
-                        Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Red), }
-                    },
-                    new Style(x => x.Nesting().Class(":pointerover").Template().OfType<Border>())
-                    {
-                        Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Green), }
-                    },
-                    new Style(x => x.Nesting().Class(":pressed").Template().OfType<Border>())
-                    {
-                        Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Blue), }
-                    },
-                }
-            };
-        }
-
-        private class TestControl : TemplatedControl
-        {
-            public IStyleable VisualChild => (IStyleable)VisualChildren[0];
-        }
-
-        private class TestClass2 : Control
-        {
-        }
-    }
-}

+ 4 - 3
tests/Avalonia.Benchmarks/Styling/Style_Apply.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Runtime.CompilerServices;
 using Avalonia.Controls;
 using Avalonia.Styling;
@@ -52,12 +53,12 @@ namespace Avalonia.Benchmarks.Styling
         {
             var target = new TestClass();
 
-            target.BeginBatchUpdate();
+            target.GetValueStore().BeginStyling();
 
             foreach (var style in _styles)
                 style.TryAttach(target, null);
 
-            target.EndBatchUpdate();
+            target.GetValueStore().EndStyling();
         }
 
         private class TestClass : Control

+ 6 - 6
tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs

@@ -32,12 +32,12 @@ namespace Avalonia.Benchmarks.Styling
         {
             var target = new TestClass();
 
-            target.BeginBatchUpdate();
+            target.GetValueStore().BeginStyling();
 
             for (var i = 0; i < 50; ++i)
                 _style.TryAttach(target, null);
 
-            target.EndBatchUpdate();
+            target.GetValueStore().EndStyling();
         }
 
         [Benchmark(OperationsPerInvoke = 50)]
@@ -45,12 +45,12 @@ namespace Avalonia.Benchmarks.Styling
         {
             var target = new TestClass();
 
-            target.BeginBatchUpdate();
+            target.GetValueStore().BeginStyling();
 
             for (var i = 0; i < 50; ++i)
                 _style.TryAttach(target, null);
 
-            target.EndBatchUpdate();
+            target.GetValueStore().EndStyling();
 
             target.Classes.Add("foo");
             target.Classes.Remove("foo");
@@ -61,12 +61,12 @@ namespace Avalonia.Benchmarks.Styling
         {
             var target = new TestClass();
 
-            target.BeginBatchUpdate();
+            target.GetValueStore().BeginStyling();
 
             for (var i = 0; i < 50; ++i)
                 _style.TryAttach(target, null);
 
-            target.EndBatchUpdate();
+            target.GetValueStore().EndStyling();
 
             target.DetachStyles();
         }

+ 13 - 8
tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs

@@ -2,7 +2,6 @@ using System.Linq;
 using System.Reactive.Linq;
 using Avalonia.Controls;
 using Avalonia.Data;
-using Avalonia.Markup.Data;
 using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Xunit;
@@ -21,7 +20,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
                 var setter = (Setter)(style.Setters.First());
 
                 Assert.IsType<Binding>(setter.Value);
-            }                
+            }
         }
 
         [Fact]
@@ -39,17 +38,23 @@ namespace Avalonia.Markup.Xaml.UnitTests
                     DataContext = data,
                 };
 
-                var setter = new Setter
+                var style = new Style()
                 {
-                    Property = TextBox.TextProperty,
-                    Value = new Binding
+                    Setters =
                     {
-                        Path = "Foo",
-                        Mode = BindingMode.TwoWay
+                        new Setter
+                        {
+                            Property = TextBox.TextProperty,
+                            Value = new Binding
+                            {
+                                Path = "Foo",
+                                Mode = BindingMode.TwoWay
+                            }
+                        }
                     }
                 };
 
-                setter.Instance(control).Start(false);
+                style.TryAttach(control, control);
                 Assert.Equal("foo", control.Text);
 
                 control.Text = "bar";

+ 1 - 1
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@@ -453,7 +453,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 
                 collection.Remove(Brushes.Green);
 
-                Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
+                Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors().ToList());
 
                 collection.Add(Brushes.Violet);
                 collection.Add(Brushes.Black);