فهرست منبع

Make ValueStore typed.

Major refactor of the Avalonia core to make the styled property store typed.
Steven Kirk 6 سال پیش
والد
کامیت
6be3acb46c
74فایلهای تغییر یافته به همراه3506 افزوده شده و 2103 حذف شده
  1. 15 11
      src/Avalonia.Animation/Animatable.cs
  2. 374 355
      src/Avalonia.Base/AvaloniaObject.cs
  3. 146 0
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  4. 58 31
      src/Avalonia.Base/AvaloniaProperty.cs
  5. 17 23
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  6. 67 0
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  7. 98 17
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  8. 32 1
      src/Avalonia.Base/AvaloniaProperty`1.cs
  9. 20 0
      src/Avalonia.Base/Data/BindingNotification.cs
  10. 13 1
      src/Avalonia.Base/Data/BindingOperations.cs
  11. 406 0
      src/Avalonia.Base/Data/BindingValue.cs
  12. 129 0
      src/Avalonia.Base/Data/Optional.cs
  13. 30 28
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  14. 88 10
      src/Avalonia.Base/DirectProperty.cs
  15. 159 0
      src/Avalonia.Base/DirectPropertyBase.cs
  16. 50 4
      src/Avalonia.Base/IAvaloniaObject.cs
  17. 0 51
      src/Avalonia.Base/IPriorityValueOwner.cs
  18. 0 9
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  19. 1 6
      src/Avalonia.Base/IStyledPropertyMetadata.cs
  20. 0 160
      src/Avalonia.Base/PriorityBindingEntry.cs
  21. 0 227
      src/Avalonia.Base/PriorityLevel.cs
  22. 0 315
      src/Avalonia.Base/PriorityValue.cs
  23. 95 0
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  24. 28 0
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  25. 18 0
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  26. 17 0
      src/Avalonia.Base/PropertyStore/IValue.cs
  27. 18 0
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  28. 143 0
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  29. 55 0
      src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs
  30. 61 0
      src/Avalonia.Base/Reactive/BindingValueAdapter.cs
  31. 35 0
      src/Avalonia.Base/Reactive/BindingValueExtensions.cs
  32. 63 0
      src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
  33. 57 0
      src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs
  34. 57 27
      src/Avalonia.Base/StyledPropertyBase.cs
  35. 0 15
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  36. 21 0
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  37. 11 0
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  38. 154 128
      src/Avalonia.Base/ValueStore.cs
  39. 12 12
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  40. 2 2
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  41. 8 8
      src/Avalonia.Controls/AutoCompleteBox.cs
  42. 9 9
      src/Avalonia.Controls/Button.cs
  43. 2 2
      src/Avalonia.Controls/Calendar/Calendar.cs
  44. 12 6
      src/Avalonia.Controls/Calendar/DatePicker.cs
  45. 5 5
      src/Avalonia.Controls/MenuItem.cs
  46. 3 3
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  47. 1 1
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  48. 12 15
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  49. 0 2
      src/Avalonia.Controls/ScrollViewer.cs
  50. 4 4
      src/Avalonia.Controls/TextBox.cs
  51. 4 3
      src/Avalonia.Layout/StackLayout.cs
  52. 18 15
      src/Avalonia.Layout/UniformGridLayout.cs
  53. 5 1
      src/Avalonia.Styling/StyledElement.cs
  54. 5 1
      src/Avalonia.Visuals/Visual.cs
  55. 1 21
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs
  56. 0 8
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  57. 220 35
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  58. 36 65
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
  59. 153 19
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  60. 16 3
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs
  61. 87 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  62. 0 156
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
  63. 31 0
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  64. 33 33
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  65. 3 13
      tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs
  66. 148 223
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  67. 1 1
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  68. 3 3
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs
  69. 4 3
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs
  70. 32 2
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  71. 32 2
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  72. 4 4
      tests/Avalonia.Styling.UnitTests/SetterTests.cs
  73. 32 2
      tests/Avalonia.Styling.UnitTests/TestControlBase.cs
  74. 32 2
      tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

+ 15 - 11
src/Avalonia.Animation/Animatable.cs

@@ -65,26 +65,30 @@ namespace Avalonia.Animation
             }
         }
 
-        /// <summary>
-        /// Reacts to a change in a <see cref="AvaloniaProperty"/> value in 
-        /// order to animate the change if a <see cref="ITransition"/> is set for the property.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
         {
-            if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return;
+            if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation)
+                return;
 
             // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
             foreach (var transition in _transitions)
             {
-                if (transition.Property == e.Property)
+                if (transition.Property == property)
                 {
-                    if (_previousTransitions.TryGetValue(e.Property, out var dispose))
+                    if (_previousTransitions.TryGetValue(property, out var dispose))
                         dispose.Dispose();
 
-                    var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
+                    var instance = transition.Apply(
+                        this,
+                        Clock ?? Avalonia.Animation.Clock.GlobalClock,
+                        oldValue.ValueOrDefault(),
+                        newValue.ValueOrDefault());
 
-                    _previousTransitions[e.Property] = instance;
+                    _previousTransitions[property] = instance;
                     return;
                 }
             }

+ 374 - 355
src/Avalonia.Base/AvaloniaObject.cs

@@ -4,11 +4,10 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
-using System.Linq;
-using System.Reactive.Linq;
 using Avalonia.Data;
 using Avalonia.Diagnostics;
 using Avalonia.Logging;
+using Avalonia.PropertyStore;
 using Avalonia.Threading;
 using Avalonia.Utilities;
 
@@ -20,13 +19,13 @@ namespace Avalonia
     /// <remarks>
     /// This class is analogous to DependencyObject in WPF.
     /// </remarks>
-    public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
+    public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
     {
         private IAvaloniaObject _inheritanceParent;
-        private List<DirectBindingSubscription> _directBindings;
+        private List<IDisposable> _directBindings;
         private PropertyChangedEventHandler _inpcChanged;
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
-        private EventHandler<AvaloniaPropertyChangedEventArgs> _inheritablePropertyChanged;
+        private List<IAvaloniaObject> _inheritanceChildren;
         private ValueStore _values;
         private ValueStore Values => _values ?? (_values = new ValueStore(this));
 
@@ -57,15 +56,6 @@ namespace Avalonia
             remove { _inpcChanged -= value; }
         }
 
-        /// <summary>
-        /// Raised when an inheritable <see cref="AvaloniaProperty"/> value changes on this object.
-        /// </summary>
-        event EventHandler<AvaloniaPropertyChangedEventArgs> IAvaloniaObject.InheritablePropertyChanged
-        {
-            add { _inheritablePropertyChanged += value; }
-            remove { _inheritablePropertyChanged -= value; }
-        }
-
         /// <summary>
         /// Gets or sets the parent object that inherited <see cref="AvaloniaProperty"/> values
         /// are inherited from.
@@ -83,47 +73,27 @@ namespace Avalonia
             set
             {
                 VerifyAccess();
+
                 if (_inheritanceParent != value)
                 {
-                    if (_inheritanceParent != null)
-                    {
-                        _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged;
-                    }
+                    var oldParent = _inheritanceParent;
+                    var valuestore = _values;
 
-                    var oldInheritanceParent = _inheritanceParent;
+                    _inheritanceParent?.RemoveInheritanceChild(this);
                     _inheritanceParent = value;
-                    var valuestore = _values;
 
                     foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
                     {
-                        if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue)
+                        if (valuestore?.IsSet(property) == true)
                         {
-                            // if local value set there can be no change
+                            // If local value set there can be no change.
                             continue;
                         }
-                        // get the value as it would have been with the previous InheritanceParent
-                        object oldValue;
-                        if (oldInheritanceParent is AvaloniaObject aobj)
-                        {
-                            oldValue = aobj.GetValueOrDefaultUnchecked(property);
-                        }
-                        else
-                        {
-                            oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
-                        }
-
-                        object newValue = GetDefaultValue(property);
 
-                        if (!Equals(oldValue, newValue))
-                        {
-                            RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
-                        }
+                        property.RouteInheritanceParentChanged(this, oldParent);
                     }
 
-                    if (_inheritanceParent != null)
-                    {
-                        _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged;
-                    }
+                    _inheritanceParent?.AddInheritanceChild(this);
                 }
             }
         }
@@ -167,9 +137,31 @@ namespace Avalonia
         public void ClearValue(AvaloniaProperty property)
         {
             Contract.Requires<ArgumentNullException>(property != null);
+            property.RouteClearValue(this);
+        }
+
+        /// <summary>
+        /// Clears a <see cref="AvaloniaProperty"/>'s local value.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        public void ClearValue<T>(AvaloniaProperty<T> property)
+        {
             VerifyAccess();
 
-            SetValue(property, AvaloniaProperty.UnsetValue);
+            switch (property)
+            {
+                case StyledPropertyBase<T> styled:
+                    _values.ClearLocalValue(styled);
+                    break;
+                case DirectPropertyBase<T> direct:
+                    var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, direct);
+                    p.InvokeSetter(this, p.GetUnsetValue(GetType()));
+                    break;
+                case null:
+                    throw new ArgumentNullException(nameof(property));
+                default:
+                    throw new NotSupportedException("Unsupported AvaloniaProperty type.");
+            }
         }
 
         /// <summary>
@@ -210,21 +202,38 @@ namespace Avalonia
         /// <returns>The value.</returns>
         public object GetValue(AvaloniaProperty property)
         {
-            if (property is null)
+            return property.RouteGetValue(this);
+        }
+
+        /// <summary>
+        /// Gets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <returns>The value.</returns>
+        public T GetValue<T>(AvaloniaProperty<T> property)
+        {
+            return property switch
             {
-                throw new ArgumentNullException(nameof(property));
-            }
+                StyledPropertyBase<T> styled => GetValue(styled),
+                DirectPropertyBase<T> direct => GetValue(direct),
+                null => throw new ArgumentNullException(nameof(property)),
+                _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
+            };
+        }
 
+        /// <summary>
+        /// Gets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <returns>The value.</returns>
+        public T GetValue<T>(StyledPropertyBase<T> property)
+        {
+            property = property ?? throw new ArgumentNullException(nameof(property));
             VerifyAccess();
 
-            if (property.IsDirect)
-            {
-                return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
-            }
-            else
-            {
-                return GetValueOrDefaultUnchecked(property);
-            }
+            return GetValueOrInheritedOrDefault(property);
         }
 
         /// <summary>
@@ -233,14 +242,13 @@ namespace Avalonia
         /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <returns>The value.</returns>
-        public T GetValue<T>(AvaloniaProperty<T> property)
+        public T GetValue<T>(DirectPropertyBase<T> property)
         {
-            if (property is null)
-            {
-                throw new ArgumentNullException(nameof(property));
-            }
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            VerifyAccess();
 
-            return (T)GetValue((AvaloniaProperty)property);
+            var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
+            return registered.InvokeGetter(this);
         }
 
         /// <summary>
@@ -284,16 +292,33 @@ namespace Avalonia
             object value,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
-            VerifyAccess();
+            property.RouteSetValue(this, value, priority);
+        }
 
-            if (property.IsDirect)
-            {
-                SetDirectValue(property, value);
-            }
-            else
+        /// <summary>
+        /// Sets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The value.</param>
+        /// <param name="priority">The priority of the value.</param>
+        public void SetValue<T>(
+            AvaloniaProperty<T> property,
+            T value,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            switch (property)
             {
-                SetStyledValue(property, value, priority);
+                case StyledPropertyBase<T> styled:
+                    SetValue(styled, value, priority);
+                    break;
+                case DirectPropertyBase<T> direct:
+                    SetValue(direct, value);
+                    break;
+                case null:
+                    throw new ArgumentNullException(nameof(property));
+                default:
+                    throw new NotSupportedException("Unsupported AvaloniaProperty type.");
             }
         }
 
@@ -305,13 +330,46 @@ namespace Avalonia
         /// <param name="value">The value.</param>
         /// <param name="priority">The priority of the value.</param>
         public void SetValue<T>(
-            AvaloniaProperty<T> property,
+            StyledPropertyBase<T> property,
             T value,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            VerifyAccess();
 
-            SetValue((AvaloniaProperty)property, value, priority);
+            LogPropertySet(property, value, priority);
+
+            if (value is UnsetValueType)
+            {
+                if (priority == BindingPriority.LocalValue)
+                {
+                    Values.ClearLocalValue(property);
+                }
+                else
+                {
+                    throw new NotSupportedException(
+                        "Canot set property to Unset at non-local value priority.");
+                }
+            }
+            else if (!(value is DoNothingType))
+            {
+                Values.SetValue(property, value, priority);
+            }
+        }
+
+        /// <summary>
+        /// Sets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The value.</param>
+        public void SetValue<T>(DirectPropertyBase<T> property, T value)
+        {
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            VerifyAccess();
+
+            LogPropertySet(property, value, BindingPriority.LocalValue);
+            SetDirectValueUnchecked(property, value);
         }
 
         /// <summary>
@@ -325,47 +383,34 @@ namespace Avalonia
         /// </returns>
         public IDisposable Bind(
             AvaloniaProperty property,
-            IObservable<object> source,
+            IObservable<BindingValue<object>> source,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
-            Contract.Requires<ArgumentNullException>(source != null);
-
-            VerifyAccess();
-
-            if (property.IsDirect)
-            {
-                if (property.IsReadOnly)
-                {
-                    throw new ArgumentException($"The property {property.Name} is readonly.");
-                }
-
-                Logger.TryGet(LogEventLevel.Verbose)?.Log(
-                    LogArea.Property,
-                    this,
-                    "Bound {Property} to {Binding} with priority LocalValue",
-                    property,
-                    GetDescription(source));
-
-                if (_directBindings == null)
-                {
-                    _directBindings = new List<DirectBindingSubscription>();
-                }
+            return property.RouteBind(this, source, priority);
+        }
 
-                return new DirectBindingSubscription(this, property, source);
-            }
-            else
+        /// <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>(
+            AvaloniaProperty<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            return property switch
             {
-                Logger.TryGet(LogEventLevel.Verbose)?.Log(
-                    LogArea.Property,
-                    this,
-                    "Bound {Property} to {Binding} with priority {Priority}",
-                    property,
-                    GetDescription(source),
-                    priority);
-
-                return Values.AddBinding(property, source, priority);
-            }
+                StyledPropertyBase<T> styled => Bind(styled, source, priority),
+                DirectPropertyBase<T> direct => Bind(direct, source),
+                null => throw new ArgumentNullException(nameof(property)),
+                _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
+            };
         }
 
         /// <summary>
@@ -379,37 +424,96 @@ namespace Avalonia
         /// A disposable which can be used to terminate the binding.
         /// </returns>
         public IDisposable Bind<T>(
-            AvaloniaProperty<T> property,
-            IObservable<T> source,
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            VerifyAccess();
 
-            return Bind(property, source.Select(x => (object)x), priority);
+            return Values.AddBinding(property, source, priority);
         }
 
         /// <summary>
-        /// Forces the specified property to be revalidated.
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
         /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
-        public void Revalidate(AvaloniaProperty property)
+        /// <param name="source">The observable.</param>
+        /// <returns>
+        /// A disposable which can be used to terminate the binding.
+        /// </returns>
+        public IDisposable Bind<T>(
+            DirectPropertyBase<T> property,
+            IObservable<BindingValue<T>> source)
         {
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            source = source ?? throw new ArgumentNullException(nameof(source));
             VerifyAccess();
-            _values?.Revalidate(property);
+
+            property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
+
+            if (property.IsReadOnly)
+            {
+                throw new ArgumentException($"The property {property.Name} is readonly.");
+            }
+
+            Logger.TryGet(LogEventLevel.Verbose)?.Log(
+                LogArea.Property,
+                this,
+                "Bound {Property} to {Binding} with priority LocalValue",
+                property,
+                GetDescription(source));
+
+            _directBindings ??= new List<IDisposable>();
+
+            return new DirectBindingSubscription<T>(this, property, source);
+        }
+
+        /// <inheritdoc/>
+        void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
+        {
+            _inheritanceChildren ??= new List<IAvaloniaObject>();
+            _inheritanceChildren.Add(child);
         }
         
-        internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
+        /// <inheritdoc/>
+        void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child)
         {
-            oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
-                GetDefaultValue(property) :
-                oldValue;
-            newValue = (newValue == AvaloniaProperty.UnsetValue) ?
-                GetDefaultValue(property) :
-                newValue;
+            _inheritanceChildren?.Remove(child);
+        }
 
-            if (!Equals(oldValue, newValue))
+        void IAvaloniaObject.InheritedPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            Optional<T> newValue)
+        {
+            if (property.Inherits && !IsSet(property))
             {
-                RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority);
+                RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
+            }
+        }
+
+        /// <inheritdoc/>
+        Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
+        {
+            return _propertyChanged?.GetInvocationList();
+        }
+
+        void IValueSink.ValueChanged<T>(
+            StyledPropertyBase<T> property,
+            BindingPriority priority,
+            Optional<T> oldValue,
+            BindingValue<T> newValue)
+        {
+            oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property);
+            newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property));
+
+            LogIfError(property, newValue);
+
+            if (!EqualityComparer<T>.Default.Equals(oldValue.Value, newValue.Value))
+            {
+                RaisePropertyChanged(property, oldValue, newValue, priority);
 
                 Logger.TryGet(LogEventLevel.Verbose)?.Log(
                     LogArea.Property,
@@ -421,39 +525,32 @@ namespace Avalonia
                     (BindingPriority)priority);
             }
         }
-        
-        internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
-        {
-            LogIfError(property, notification);
-            UpdateDataValidation(property, notification);
-        }
 
-        /// <inheritdoc/>
-        Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
-        {
-            return _propertyChanged?.GetInvocationList();
-        }
+        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { }
 
         /// <summary>
-        /// Gets all priority values set on the object.
+        /// Called for each inherited property when the <see cref="InheritanceParent"/> changes.
         /// </summary>
-        /// <returns>A collection of property/value tuples.</returns>
-        internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
-
-        /// <summary>
-        /// Forces revalidation of properties when a property value changes.
-        /// </summary>
-        /// <param name="property">The property to that affects validation.</param>
-        /// <param name="affected">The affected properties.</param>
-        protected static void AffectsValidation(AvaloniaProperty property, params AvaloniaProperty[] affected)
+        /// <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,
+            IAvaloniaObject oldParent)
         {
-            property.Changed.Subscribe(e =>
+            var oldValue = oldParent switch
             {
-                foreach (var p in affected)
-                {
-                    e.Sender.Revalidate(p);
-                }
-            });
+                AvaloniaObject o => o.GetValueOrInheritedOrDefault(property),
+                null => property.GetDefaultValue(GetType()),
+                _ => oldParent.GetValue(property)
+            };
+
+            var newValue = GetInheritedOrDefault(property);
+
+            if (!EqualityComparer<T>.Default.Equals(oldValue, newValue))
+            {
+                RaisePropertyChanged(property, oldValue, newValue);
+            }
         }
 
         /// <summary>
@@ -477,18 +574,25 @@ namespace Avalonia
         /// enabled.
         /// </summary>
         /// <param name="property">The property.</param>
-        /// <param name="status">The new validation status.</param>
-        protected virtual void UpdateDataValidation(
-            AvaloniaProperty property,
-            BindingNotification status)
+        /// <param name="value">The new binding value for the property.</param>
+        protected virtual void UpdateDataValidation<T>(
+            AvaloniaProperty<T> property,
+            BindingValue<T> value)
         {
         }
 
         /// <summary>
         /// Called when a avalonia property changes on the object.
         /// </summary>
-        /// <param name="e">The event arguments.</param>
-        protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        /// <param name="property">The property whose value has changed.</param>
+        /// <param name="oldValue">The old value of the property.</param>
+        /// <param name="newValue">The new value of the property.</param>
+        /// <param name="priority">The priority of the new value.</param>
+        protected virtual void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
         {
         }
 
@@ -499,40 +603,57 @@ 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(
-            AvaloniaProperty property,
-            object oldValue,
-            object newValue,
+        protected internal void RaisePropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
-            VerifyAccess();
+            property = property ?? throw new ArgumentNullException(nameof(property));
 
-            AvaloniaPropertyChangedEventArgs e = new AvaloniaPropertyChangedEventArgs(
-                this,
-                property,
-                oldValue,
-                newValue,
-                priority);
+            VerifyAccess();
 
             property.Notifying?.Invoke(this, true);
 
             try
             {
-                OnPropertyChanged(e);
-                property.NotifyChanged(e);
+                AvaloniaPropertyChangedEventArgs<T> e = null;
+                var hasChanged = property.HasChangedSubscriptions;
+
+                if (hasChanged || _propertyChanged != null)
+                {
+                    e = new AvaloniaPropertyChangedEventArgs<T>(
+                        this,
+                        property,
+                        oldValue,
+                        newValue,
+                        priority);
+                }
+
+                OnPropertyChanged(property, oldValue, newValue, priority);
+
+                if (hasChanged)
+                {
+                    property.NotifyChanged(e);
+                }
 
                 _propertyChanged?.Invoke(this, e);
 
                 if (_inpcChanged != null)
                 {
-                    PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
-                    _inpcChanged(this, e2);
+                    var inpce = new PropertyChangedEventArgs(property.Name);
+                    _inpcChanged(this, inpce);
                 }
 
-                if (property.Inherits)
+                if (property.Inherits && _inheritanceChildren != null)
                 {
-                    _inheritablePropertyChanged?.Invoke(this, e);
+                    foreach (var child in _inheritanceChildren)
+                    {
+                        child.InheritedPropertyChanged(
+                            property,
+                            oldValue,
+                            newValue.ToOptional());
+                    }
                 }
             }
             finally
@@ -561,216 +682,103 @@ namespace Avalonia
                 return false;
             }
 
-            DeferredSetter<T> setter = Values.GetDirectDeferredSetter(property);
-
-            return setter.SetAndNotify(this, property, ref field, value);
+            var old = field;
+            field = value;
+            RaisePropertyChanged(property, old, value);
+            return true;
         }
 
-        /// <summary>
-        /// Tries to cast a value to a type, taking into account that the value may be a
-        /// <see cref="BindingNotification"/>.
-        /// </summary>
-        /// <param name="value">The value.</param>
-        /// <param name="type">The type.</param>
-        /// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns>
-        private static object CastOrDefault(object value, Type type)
+        private T GetInheritedOrDefault<T>(StyledPropertyBase<T> property)
         {
-            var notification = value as BindingNotification;
-
-            if (notification == null)
+            if (property.Inherits && InheritanceParent is AvaloniaObject o)
             {
-                return TypeUtilities.ConvertImplicitOrDefault(value, type);
+                return o.GetValueOrInheritedOrDefault(property);
             }
-            else
-            {
-                if (notification.HasValue)
-                {
-                    notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type));
-                }
 
-                return notification;
-            }
+            return property.GetDefaultValue(GetType());
         }
 
-        /// <summary>
-        /// Gets the default value for a property.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <returns>The default value.</returns>
-        private object GetDefaultValue(AvaloniaProperty property)
+        private T GetValueOrInheritedOrDefault<T>(StyledPropertyBase<T> property)
         {
-            if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
-                return aobj.GetValueOrDefaultUnchecked(property);
-            return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
-        }
+            var o = this;
+            var inherits = property.Inherits;
+            var value = default(T);
 
-        /// <summary>
-        /// Gets the value or default value for a property.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <returns>The default value.</returns>
-        private object GetValueOrDefaultUnchecked(AvaloniaProperty property)
-        {
-            var aobj = this;
-            var valuestore = aobj._values;
-            if (valuestore != null)
+            while (o != null)
             {
-                var result = valuestore.GetValue(property);
-                if (result != AvaloniaProperty.UnsetValue)
-                {
-                    return result;
-                }
-            }
-            if (property.Inherits)
-            {
-                while (aobj.InheritanceParent is AvaloniaObject parent)
-                {
-                    aobj = parent;
-                    valuestore = aobj._values;
-                    if (valuestore != null)
-                    {
-                        var result = valuestore.GetValue(property);
-                        if (result != AvaloniaProperty.UnsetValue)
-                        {
-                            return result;
-                        }
-                    }
-                }
-            }
-            return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
-        }
-
-        /// <summary>
-        /// Sets the value of a direct property.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <param name="value">The value.</param>
-        private void SetDirectValue(AvaloniaProperty property, object value)
-        {
-            void Set()
-            {
-                var notification = value as BindingNotification;
+                var values = o._values;
 
-                if (notification != null)
+                if (values?.TryGetValue(property, out value) == true)
                 {
-                    LogIfError(property, notification);
-                    value = notification.Value;
+                    return value;
                 }
 
-                if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue)
+                if (!inherits)
                 {
-                    var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType());
-                    var accessor = (IDirectPropertyAccessor)GetRegistered(property);
-                    var finalValue = value == AvaloniaProperty.UnsetValue ?
-                        metadata.UnsetValue : value;
-
-                    LogPropertySet(property, value, BindingPriority.LocalValue);
-
-                    accessor.SetValue(this, finalValue);
+                    break;
                 }
 
-                if (notification != null)
-                {
-                    UpdateDataValidation(property, notification);
-                }
+                o = o.InheritanceParent as AvaloniaObject;
             }
 
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                Set();
-            }
-            else
-            {
-                Dispatcher.UIThread.Post(Set);
-            }
+            return property.GetDefaultValue(GetType());
         }
 
         /// <summary>
-        /// Sets the value of a styled property.
+        /// Sets the value of a direct property.
         /// </summary>
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
-        /// <param name="priority">The priority of the value.</param>
-        private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority)
+        private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value)
         {
-            var notification = value as BindingNotification;
+            var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
 
-            // We currently accept BindingNotifications for non-direct properties but we just
-            // strip them to their underlying value.
-            if (notification != null)
+            if (value is UnsetValueType)
             {
-                if (!notification.HasValue)
-                {
-                    return;
-                }
-                else
-                {
-                    value = notification.Value;
-                }
+                p.InvokeSetter(this, p.GetUnsetValue(GetType()));
             }
-
-            var originalValue = value;
-
-            if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value))
+            else if (!(value is DoNothingType))
             {
-                throw new ArgumentException(string.Format(
-                    "Invalid value for Property '{0}': '{1}' ({2})",
-                    property.Name,
-                    originalValue,
-                    originalValue?.GetType().FullName ?? "(null)"));
+                p.InvokeSetter(this, value);
             }
-
-            LogPropertySet(property, value, priority);
-            Values.AddValue(property, value, (int)priority);
         }
 
         /// <summary>
-        /// Given a direct property, returns a registered avalonia property that is equivalent or
-        /// throws if not found.
+        /// Sets the value of a direct property.
         /// </summary>
         /// <param name="property">The property.</param>
-        /// <returns>The registered property.</returns>
-        private AvaloniaProperty GetRegistered(AvaloniaProperty property)
+        /// <param name="value">The value.</param>
+        private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
         {
-            var direct = property as IDirectPropertyAccessor;
-
-            if (direct == null)
-            {
-                throw new AvaloniaInternalException(
-                    "AvaloniaObject.GetRegistered should only be called for direct properties");
-            }
+            var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property);
 
-            if (property.OwnerType.IsAssignableFrom(GetType()))
+            if (p == null)
             {
-                return property;
+                throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
             }
 
-            var result =  AvaloniaPropertyRegistry.Instance.GetRegistered(this)
-                .FirstOrDefault(x => x == property);
+            LogIfError(property, value);
 
-            if (result == null)
+            switch (value.Type)
             {
-                throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
+                case BindingValueType.UnsetValue:
+                case BindingValueType.BindingError:
+                    var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType()));
+                    property.InvokeSetter(this, fallback);
+                    break;
+                case BindingValueType.DataValidationError:
+                    property.InvokeSetter(this, value);
+                    break;
+                case BindingValueType.Value:
+                case BindingValueType.BindingErrorWithFallback:
+                case BindingValueType.DataValidationErrorWithFallback:
+                    property.InvokeSetter(this, value);
+                    break;
             }
 
-            return result;
-        }
-
-        /// <summary>
-        /// Called when a property is changed on the current <see cref="InheritanceParent"/>.
-        /// </summary>
-        /// <param name="sender">The event sender.</param>
-        /// <param name="e">The event args.</param>
-        /// <remarks>
-        /// Checks for changes in an inherited property value.
-        /// </remarks>
-        private void ParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
-        {
-            Contract.Requires<ArgumentNullException>(e != null);
-
-            if (e.Property.Inherits && !IsSet(e.Property))
+            if (p.IsDataValidationEnabled)
             {
-                RaisePropertyChanged(e.Property, e.OldValue, e.NewValue, BindingPriority.LocalValue);
+                UpdateDataValidation(property, value);
             }
         }
 
@@ -779,7 +787,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="o">The observable.</param>
         /// <returns>The description.</returns>
-        private string GetDescription(IObservable<object> o)
+        private string GetDescription(object o)
         {
             var description = o as IDescription;
             return description?.Description ?? o.ToString();
@@ -789,12 +797,12 @@ namespace Avalonia
         /// Logs a mesage if the notification represents a binding error.
         /// </summary>
         /// <param name="property">The property being bound.</param>
-        /// <param name="notification">The binding notification.</param>
-        private void LogIfError(AvaloniaProperty property, BindingNotification notification)
+        /// <param name="value">The binding notification.</param>
+        private void LogIfError<T>(AvaloniaProperty property, BindingValue<T> value)
         {
-            if (notification.ErrorType == BindingErrorType.Error)
+            if (value.HasError)
             {
-                if (notification.Error is AggregateException aggregate)
+                if (value.Error is AggregateException aggregate)
                 {
                     foreach (var inner in aggregate.InnerExceptions)
                     {
@@ -803,7 +811,7 @@ namespace Avalonia
                 }
                 else
                 {
-                    LogBindingError(property, notification.Error);
+                    LogBindingError(property, value.Error);
                 }
             }
         }
@@ -814,7 +822,7 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         /// <param name="value">The new value.</param>
         /// <param name="priority">The priority.</param>
-        private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority)
+        private void LogPropertySet<T>(AvaloniaProperty<T> property, T value, BindingPriority priority)
         {
             Logger.TryGet(LogEventLevel.Verbose)?.Log(
                 LogArea.Property,
@@ -825,16 +833,16 @@ namespace Avalonia
                 priority);
         }
 
-        private class DirectBindingSubscription : IObserver<object>, IDisposable
+        private class DirectBindingSubscription<T> : IObserver<BindingValue<T>>, IDisposable
         {
-            readonly AvaloniaObject _owner;
-            readonly AvaloniaProperty _property;
-            IDisposable _subscription;
+            private readonly AvaloniaObject _owner;
+            private readonly DirectPropertyBase<T> _property;
+            private readonly IDisposable _subscription;
 
             public DirectBindingSubscription(
                 AvaloniaObject owner,
-                AvaloniaProperty property,
-                IObservable<object> source)
+                DirectPropertyBase<T> property,
+                IObservable<BindingValue<T>> source)
             {
                 _owner = owner;
                 _property = property;
@@ -850,11 +858,22 @@ namespace Avalonia
 
             public void OnCompleted() => Dispose();
             public void OnError(Exception error) => Dispose();
-
-            public void OnNext(object value)
+            public void OnNext(BindingValue<T> value)
             {
-                var castValue = CastOrDefault(value, _property.PropertyType);
-                _owner.SetDirectValue(_property, castValue);
+                if (Dispatcher.UIThread.CheckAccess())
+                {
+                    _owner.SetDirectValueUnchecked(_property, 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 = _owner;
+                    var property = _property;
+                    var newValue = value;
+
+                    Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue));
+                }
             }
         }
     }

+ 146 - 0
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -68,6 +68,51 @@ namespace Avalonia
             return new AvaloniaPropertyObservable<T>(o, property);
         }
 
+        /// <summary>
+        /// Gets an observable for a <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <returns>
+        /// An observable which fires immediately with the current value of the property on the
+        /// object and subsequently each time the property value changes.
+        /// </returns>
+        /// <remarks>
+        /// The subscription to <paramref name="o"/> is created using a weak reference.
+        /// </remarks>
+        public static IObservable<BindingValue<object>> GetBindingObservable(
+            this IAvaloniaObject o,
+            AvaloniaProperty property)
+        {
+            Contract.Requires<ArgumentNullException>(o != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+
+            return new AvaloniaPropertyBindingObservable<object>(o, property);
+        }
+
+        /// <summary>
+        /// Gets an observable for a <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <typeparam name="T">The property type.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <returns>
+        /// An observable which fires immediately with the current value of the property on the
+        /// object and subsequently each time the property value changes.
+        /// </returns>
+        /// <remarks>
+        /// The subscription to <paramref name="o"/> is created using a weak reference.
+        /// </remarks>
+        public static IObservable<BindingValue<T>> GetBindingObservable<T>(
+            this IAvaloniaObject o,
+            AvaloniaProperty<T> property)
+        {
+            Contract.Requires<ArgumentNullException>(o != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+
+            return new AvaloniaPropertyBindingObservable<T>(o, property);
+        }
+
         /// <summary>
         /// Gets an observable that listens for property changed events for an
         /// <see cref="AvaloniaProperty"/>.
@@ -134,6 +179,107 @@ namespace Avalonia
                 o.GetObservable(property));
         }
 
+        /// <summary>
+        /// Gets a subject for a <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="priority">
+        /// The priority with which binding values are written to the object.
+        /// </param>
+        /// <returns>
+        /// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the 
+        /// property.
+        /// </returns>
+        public static ISubject<BindingValue<object>> GetBindingSubject(
+            this IAvaloniaObject o,
+            AvaloniaProperty property,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            return Subject.Create<BindingValue<object>>(
+                Observer.Create<BindingValue<object>>(x =>
+                {
+                    if (x.HasValue)
+                    {
+                        o.SetValue(property, x.Value, priority);
+                    }
+                }),
+                o.GetBindingObservable(property));
+        }
+
+        /// <summary>
+        /// Gets a subject for a <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <typeparam name="T">The property type.</typeparam>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="priority">
+        /// The priority with which binding values are written to the object.
+        /// </param>
+        /// <returns>
+        /// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the 
+        /// property.
+        /// </returns>
+        public static ISubject<BindingValue<T>> GetBindingSubject<T>(
+            this IAvaloniaObject o,
+            AvaloniaProperty<T> property,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            return Subject.Create<BindingValue<T>>(
+                Observer.Create<BindingValue<T>>(x =>
+                {
+                    if (x.HasValue)
+                    {
+                        o.SetValue(property, x.Value, priority);
+                    }
+                }),
+                o.GetBindingObservable(property));
+        }
+
+        /// <summary>
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <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 static IDisposable Bind(
+            this IAvaloniaObject target,
+            AvaloniaProperty property,
+            IObservable<object> source,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            return target.Bind(
+                property,
+                source.ToBindingValue(),
+                priority);
+        }
+
+        /// <summary>
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <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 static IDisposable Bind<T>(
+            this IAvaloniaObject target,
+            AvaloniaProperty<T> property,
+            IObservable<T> source,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            return target.Bind(
+                property,
+                source.ToBindingValue(),
+                priority);
+        }
+
         /// <summary>
         /// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
         /// </summary>

+ 58 - 31
src/Avalonia.Base/AvaloniaProperty.cs

@@ -14,7 +14,7 @@ namespace Avalonia
     /// <summary>
     /// Base class for avalonia properties.
     /// </summary>
-    public class AvaloniaProperty : IEquatable<AvaloniaProperty>
+    public abstract class AvaloniaProperty : IEquatable<AvaloniaProperty>
     {
         /// <summary>
         /// Represents an unset property value.
@@ -183,6 +183,8 @@ namespace Avalonia
         /// </summary>
         internal int Id { get; }
 
+        internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false;
+
         /// <summary>
         /// Provides access to a property's binding via the <see cref="AvaloniaObject"/>
         /// indexer.
@@ -255,7 +257,6 @@ namespace Avalonia
         /// <param name="defaultValue">The default value of the property.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
-        /// <param name="validate">A validation function.</param>
         /// <param name="notifying">
         /// A method that gets called before and after the property starts being notified on an
         /// object; the bool argument will be true before and false afterwards. This callback is
@@ -267,7 +268,6 @@ namespace Avalonia
             TValue defaultValue = default(TValue),
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
-            Func<TOwner, TValue, TValue> validate = null,
             Action<IAvaloniaObject, bool> notifying = null)
                 where TOwner : IAvaloniaObject
         {
@@ -275,7 +275,6 @@ namespace Avalonia
 
             var metadata = new StyledPropertyMetadata<TValue>(
                 defaultValue,
-                validate: Cast(validate),
                 defaultBindingMode: defaultBindingMode);
 
             var result = new StyledProperty<TValue>(
@@ -298,7 +297,6 @@ namespace Avalonia
         /// <param name="defaultValue">The default value of the property.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
-        /// <param name="validate">A validation function.</param>
         /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
         public static AttachedProperty<TValue> RegisterAttached<TOwner, THost, TValue>(
             string name,
@@ -312,7 +310,6 @@ namespace Avalonia
 
             var metadata = new StyledPropertyMetadata<TValue>(
                 defaultValue,
-                validate: Cast(validate),
                 defaultBindingMode: defaultBindingMode);
 
             var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits);
@@ -332,7 +329,6 @@ namespace Avalonia
         /// <param name="defaultValue">The default value of the property.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
-        /// <param name="validate">A validation function.</param>
         /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
         public static AttachedProperty<TValue> RegisterAttached<THost, TValue>(
             string name,
@@ -347,7 +343,6 @@ namespace Avalonia
 
             var metadata = new StyledPropertyMetadata<TValue>(
                 defaultValue,
-                validate: Cast(validate),
                 defaultBindingMode: defaultBindingMode);
 
             var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits);
@@ -365,9 +360,7 @@ namespace Avalonia
         /// <param name="name">The name of the property.</param>
         /// <param name="getter">Gets the current value of the property.</param>
         /// <param name="setter">Sets the value of the property.</param>
-        /// <param name="unsetValue">
-        /// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
-        /// </param>
+        /// <param name="unsetValue">The value to use when the property is cleared.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
         /// <param name="enableDataValidation">
         /// Whether the property is interested in data validation.
@@ -383,13 +376,18 @@ namespace Avalonia
                 where TOwner : IAvaloniaObject
         {
             Contract.Requires<ArgumentNullException>(name != null);
+            Contract.Requires<ArgumentNullException>(getter != null);
 
             var metadata = new DirectPropertyMetadata<TValue>(
                 unsetValue: unsetValue,
-                defaultBindingMode: defaultBindingMode,
-                enableDataValidation: enableDataValidation);
+                defaultBindingMode: defaultBindingMode);
 
-            var result = new DirectProperty<TOwner, TValue>(name, getter, setter, metadata);
+            var result = new DirectProperty<TOwner, TValue>(
+                name,
+                getter,
+                setter,
+                metadata,
+                enableDataValidation);
             AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
             return result;
         }
@@ -483,6 +481,12 @@ namespace Avalonia
         /// </summary>
         internal bool HasNotifyInitializedObservers => _initialized.HasObservers;
 
+        /// <summary>
+        /// Notifies the <see cref="Initialized"/> observable.
+        /// </summary>
+        /// <param name="o">The object being initialized.</param>
+        internal abstract void NotifyInitialized(IAvaloniaObject o);
+
         /// <summary>
         /// Notifies the <see cref="Initialized"/> observable.
         /// </summary>
@@ -501,6 +505,42 @@ namespace Avalonia
             _changed.OnNext(e);
         }
 
+        /// <summary>
+        /// Routes an untyped ClearValue call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        internal abstract void RouteClearValue(IAvaloniaObject o);
+
+        /// <summary>
+        /// Routes an untyped GetValue call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        internal abstract object RouteGetValue(IAvaloniaObject o);
+
+        /// <summary>
+        /// Routes an untyped SetValue call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        /// <param name="value">The value.</param>
+        /// <param name="priority">The priority.</param>
+        internal abstract void RouteSetValue(
+            IAvaloniaObject o,
+            object value,
+            BindingPriority priority);
+
+        /// <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(
+            IAvaloniaObject o,
+            IObservable<BindingValue<object>> source,
+            BindingPriority priority);
+
+        internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent);
+
         /// <summary>
         /// Overrides the metadata for the property on the specified type.
         /// </summary>
@@ -555,28 +595,15 @@ namespace Avalonia
 
             return _defaultMetadata;
         }
-
-        [DebuggerHidden]
-        private static Func<IAvaloniaObject, TValue, TValue> Cast<TOwner, TValue>(Func<TOwner, TValue, TValue> f)
-            where TOwner : IAvaloniaObject
-        {
-            if (f != null)
-            {
-                return (o, v) => (o is TOwner) ? f((TOwner)o, v) : v;
-            }
-            else
-            {
-                return null;
-            }
-        }
-
-        
     }
+
     /// <summary>
     /// Class representing the <see cref="AvaloniaProperty.UnsetValue"/>.
     /// </summary>
-    public class UnsetValueType
+    public sealed class UnsetValueType
     {
+        internal UnsetValueType() { }
+
         /// <summary>
         /// Returns the string representation of the <see cref="AvaloniaProperty.UnsetValue"/>.
         /// </summary>

+ 17 - 23
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@@ -4,32 +4,20 @@
 using System;
 using Avalonia.Data;
 
+#nullable enable
+
 namespace Avalonia
 {
     /// <summary>
     /// Provides information for a avalonia property change.
     /// </summary>
-    public class AvaloniaPropertyChangedEventArgs : EventArgs
+    public abstract class AvaloniaPropertyChangedEventArgs : EventArgs
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AvaloniaPropertyChangedEventArgs"/> class.
-        /// </summary>
-        /// <param name="sender">The object that the property changed on.</param>
-        /// <param name="property">The property that changed.</param>
-        /// <param name="oldValue">The old value of the property.</param>
-        /// <param name="newValue">The new value of the property.</param>
-        /// <param name="priority">The priority of the binding that produced the value.</param>
         public AvaloniaPropertyChangedEventArgs(
-            AvaloniaObject sender,
-            AvaloniaProperty property,
-            object oldValue,
-            object newValue,
+            IAvaloniaObject sender,
             BindingPriority priority)
         {
             Sender = sender;
-            Property = property;
-            OldValue = oldValue;
-            NewValue = newValue;
             Priority = priority;
         }
 
@@ -37,7 +25,7 @@ namespace Avalonia
         /// Gets the <see cref="AvaloniaObject"/> that the property changed on.
         /// </summary>
         /// <value>The sender object.</value>
-        public AvaloniaObject Sender { get; private set; }
+        public IAvaloniaObject Sender { get; }
 
         /// <summary>
         /// Gets the property that changed.
@@ -45,30 +33,36 @@ namespace Avalonia
         /// <value>
         /// The property that changed.
         /// </value>
-        public AvaloniaProperty Property { get; private set; }
+        public AvaloniaProperty Property => GetProperty();
 
         /// <summary>
         /// Gets the old value of the property.
         /// </summary>
         /// <value>
-        /// The old value of the property.
+        /// The old value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
+        /// property previously had no value.
         /// </value>
-        public object OldValue { get; private set; }
+        public object? OldValue => GetOldValue();
 
         /// <summary>
         /// Gets the new value of the property.
         /// </summary>
         /// <value>
-        /// The new value of the property.
+        /// The new value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
+        /// property previously had no value.
         /// </value>
-        public object NewValue { get; private set; }
+        public object? NewValue => GetNewValue();
 
         /// <summary>
         /// Gets the priority of the binding that produced the value.
         /// </summary>
         /// <value>
-        /// The priority of the binding that produced the value.
+        /// The priority of the new value.
         /// </value>
         public BindingPriority Priority { get; private set; }
+
+        protected abstract AvaloniaProperty GetProperty();
+        protected abstract object? GetOldValue();
+        protected abstract object? GetNewValue();
     }
 }

+ 67 - 0
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs

@@ -0,0 +1,67 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Provides information for a avalonia property change.
+    /// </summary>
+    public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvaloniaPropertyChangedEventArgs"/> class.
+        /// </summary>
+        /// <param name="sender">The object that the property changed on.</param>
+        /// <param name="property">The property that changed.</param>
+        /// <param name="oldValue">The old value of the property.</param>
+        /// <param name="newValue">The new value of the property.</param>
+        /// <param name="priority">The priority of the binding that produced the value.</param>
+        public AvaloniaPropertyChangedEventArgs(
+            IAvaloniaObject sender,
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+            : base(sender, priority)
+        {
+            Property = property;
+            OldValue = oldValue;
+            NewValue = newValue;
+        }
+
+        /// <summary>
+        /// Gets the property that changed.
+        /// </summary>
+        /// <value>
+        /// The property that changed.
+        /// </value>
+        public new AvaloniaProperty<T> Property { get; }
+
+        /// <summary>
+        /// Gets the old value of the property.
+        /// </summary>
+        /// <value>
+        /// The old value of the property.
+        /// </value>
+        public new Optional<T> OldValue { get; private set; }
+
+        /// <summary>
+        /// Gets the new value of the property.
+        /// </summary>
+        /// <value>
+        /// The new value of the property.
+        /// </value>
+        public new BindingValue<T> NewValue { get; private set; }
+
+        protected override AvaloniaProperty GetProperty() => Property;
+
+        protected override object? GetOldValue() => OldValue.ValueOrDefault(AvaloniaProperty.UnsetValue);
+
+        protected override object? GetNewValue() => NewValue.ValueOrDefault(AvaloniaProperty.UnsetValue);
+    }
+}

+ 98 - 17
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -20,10 +20,14 @@ namespace Avalonia
             new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
         private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _attached =
             new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
+        private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _direct =
+            new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _registeredCache =
             new Dictionary<Type, List<AvaloniaProperty>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _attachedCache =
             new Dictionary<Type, List<AvaloniaProperty>>();
+        private readonly Dictionary<Type, List<AvaloniaProperty>> _directCache =
+            new Dictionary<Type, List<AvaloniaProperty>>();
         private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
             new Dictionary<Type, List<PropertyInitializationData>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
@@ -105,6 +109,37 @@ namespace Avalonia
             return result;
         }
 
+        /// <summary>
+        /// Gets all direct <see cref="AvaloniaProperty"/>s registered on a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
+        public IEnumerable<AvaloniaProperty> GetRegisteredDirect(Type type)
+        {
+            Contract.Requires<ArgumentNullException>(type != null);
+
+            if (_directCache.TryGetValue(type, out var result))
+            {
+                return result;
+            }
+
+            var t = type;
+            result = new List<AvaloniaProperty>();
+
+            while (t != null)
+            {
+                if (_direct.TryGetValue(t, out var direct))
+                {
+                    result.AddRange(direct.Values);
+                }
+
+                t = t.BaseType;
+            }
+
+            _directCache.Add(type, result);
+            return result;
+        }
+
         /// <summary>
         /// Gets all inherited <see cref="AvaloniaProperty"/>s registered on a type.
         /// </summary>
@@ -150,13 +185,29 @@ namespace Avalonia
         /// </summary>
         /// <param name="o">The object.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegistered(AvaloniaObject o)
+        public IEnumerable<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
         {
             Contract.Requires<ArgumentNullException>(o != null);
 
             return GetRegistered(o.GetType());
         }
 
+        /// <summary>
+        /// Finds a direct property as registered on an object.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The direct property.</param>
+        /// <returns>
+        /// The registered property or null if no matching property found.
+        /// </returns>
+        public DirectPropertyBase<T> GetRegisteredDirect<T>(
+            IAvaloniaObject o,
+            DirectPropertyBase<T> property)
+        {
+            return FindRegisteredDirect(o, property) ??
+                throw new ArgumentException($"Property '{property.Name} not registered on '{o.GetType()}");
+        }
+
         /// <summary>
         /// Finds a registered property on a type by name.
         /// </summary>
@@ -192,7 +243,7 @@ namespace Avalonia
         /// <exception cref="InvalidOperationException">
         /// The property name contains a '.'.
         /// </exception>
-        public AvaloniaProperty FindRegistered(AvaloniaObject o, string name)
+        public AvaloniaProperty FindRegistered(IAvaloniaObject o, string name)
         {
             Contract.Requires<ArgumentNullException>(o != null);
             Contract.Requires<ArgumentNullException>(name != null);
@@ -200,6 +251,34 @@ namespace Avalonia
             return FindRegistered(o.GetType(), name);
         }
 
+        /// <summary>
+        /// Finds a direct property as registered on an object.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The direct property.</param>
+        /// <returns>
+        /// The registered property or null if no matching property found.
+        /// </returns>
+        public DirectPropertyBase<T> FindRegisteredDirect<T>(
+            IAvaloniaObject o,
+            DirectPropertyBase<T> property)
+        {
+            if (property.Owner == o.GetType())
+            {
+                return property;
+            }
+
+            foreach (var p in GetRegisteredDirect(o.GetType()))
+            {
+                if (p == property)
+                {
+                    return (DirectPropertyBase<T>)p;
+                }
+            }
+
+            return null;
+        }
+
         /// <summary>
         /// Finds a registered property by Id.
         /// </summary>
@@ -265,6 +344,22 @@ namespace Avalonia
                 inner.Add(property.Id, property);
             }
 
+            if (property.IsDirect)
+            {
+                if (!_direct.TryGetValue(type, out inner))
+                {
+                    inner = new Dictionary<int, AvaloniaProperty>();
+                    inner.Add(property.Id, property);
+                    _direct.Add(type, inner);
+                }
+                else if (!inner.ContainsKey(property.Id))
+                {
+                    inner.Add(property.Id, property);
+                }
+
+                _directCache.Clear();
+            }
+
             if (!_properties.ContainsKey(property.Id))
             {
                 _properties.Add(property.Id, property);
@@ -318,18 +413,6 @@ namespace Avalonia
 
             var type = o.GetType();
 
-            void Notify(AvaloniaProperty property, object value)
-            {
-                var e = new AvaloniaPropertyChangedEventArgs(
-                    o,
-                    property,
-                    AvaloniaProperty.UnsetValue,
-                    value,
-                    BindingPriority.Unset);
-
-                property.NotifyInitialized(e);
-            }
-
             if (!_initializedCache.TryGetValue(type, out var initializationData))
             {
                 var visited = new HashSet<AvaloniaProperty>();
@@ -370,9 +453,7 @@ namespace Avalonia
                     continue;
                 }
 
-                object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value;
-
-                Notify(data.Property, value);
+                data.Property.NotifyInitialized(o);
             }
         }
 

+ 32 - 1
src/Avalonia.Base/AvaloniaProperty`1.cs

@@ -2,6 +2,8 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Data;
+using Avalonia.Utilities;
 
 namespace Avalonia
 {
@@ -9,7 +11,7 @@ namespace Avalonia
     /// A typed avalonia property.
     /// </summary>
     /// <typeparam name="TValue">The value type of the property.</typeparam>
-    public class AvaloniaProperty<TValue> : AvaloniaProperty
+    public abstract class AvaloniaProperty<TValue> : AvaloniaProperty
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
@@ -40,5 +42,34 @@ namespace Avalonia
             : base(source, ownerType, metadata)
         {
         }
+
+        internal override void RouteClearValue(IAvaloniaObject o)
+        {
+            o.ClearValue<TValue>(this);
+        }
+
+        protected BindingValue<object> TryConvert(object value)
+        {
+            if (value == UnsetValue)
+            {
+                return BindingValue<object>.Unset;
+            }
+            else if (value == BindingOperations.DoNothing)
+            {
+                return BindingValue<object>.DoNothing;
+            }
+
+            if (!TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted))
+            {
+                var error = new ArgumentException(string.Format(
+                    "Invalid value for Property '{0}': '{1}' ({2})",
+                    Name,
+                    value,
+                    value?.GetType().FullName ?? "(null)"));
+                return BindingValue<object>.BindingError(error);
+            }
+
+            return converted;
+        }
     }
 }

+ 20 - 0
src/Avalonia.Base/Data/BindingNotification.cs

@@ -236,6 +236,26 @@ namespace Avalonia.Data
             _value = value;
         }
 
+        public BindingValue<object> ToBindingValue()
+        {
+            if (ErrorType == BindingErrorType.None)
+            {
+                return HasValue ? new BindingValue<object>(Value) : BindingValue<object>.Unset;
+            }
+            else if (ErrorType == BindingErrorType.Error)
+            {
+                return BindingValue<object>.BindingError(
+                    Error,
+                    HasValue ? new Optional<object>(Value) : Optional<object>.Empty);
+            }
+            else
+            {
+                return BindingValue<object>.DataValidationError(
+                    Error,
+                    HasValue ? new Optional<object>(Value) : Optional<object>.Empty);
+            }
+        }
+
         /// <inheritdoc/>
         public override string ToString()
         {

+ 13 - 1
src/Avalonia.Base/Data/BindingOperations.cs

@@ -5,12 +5,13 @@ using System;
 using System.Linq;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
+using Avalonia.Reactive;
 
 namespace Avalonia.Data
 {
     public static class BindingOperations
     {
-        public static readonly object DoNothing = new object();
+        public static readonly object DoNothing = new DoNothingType();
 
         /// <summary>
         /// Applies an <see cref="InstancedBinding"/> a property on an <see cref="IAvaloniaObject"/>.
@@ -77,4 +78,15 @@ namespace Avalonia.Data
             }
         }
     }
+
+    public sealed class DoNothingType
+    {
+        internal DoNothingType() { }
+
+        /// <summary>
+        /// Returns the string representation of <see cref="BindingOperations.DoNothing"/>.
+        /// </summary>
+        /// <returns>The string "(do nothing)".</returns>
+        public override string ToString() => "(do nothing)";
+    }
 }

+ 406 - 0
src/Avalonia.Base/Data/BindingValue.cs

@@ -0,0 +1,406 @@
+using System;
+using Avalonia.Utilities;
+
+#nullable enable
+
+namespace Avalonia.Data
+{
+    /// <summary>
+    /// Describes the type of a <see cref="BindingValue{T}"/>.
+    /// </summary>
+    public enum BindingValueType
+    {
+        /// <summary>
+        /// An unset value: the target property will revert to its unbound state until a new
+        /// binding value is produced.
+        /// </summary>
+        UnsetValue = 0,
+
+        /// <summary>
+        /// Do nothing: the binding value will be ignored.
+        /// </summary>
+        DoNothing = 1,
+
+        /// <summary>
+        /// A simple value.
+        /// </summary>
+        Value = 2 | HasValue,
+
+        /// <summary>
+        /// A binding error, such as a missing source property.
+        /// </summary>
+        BindingError = 3 | HasError,
+
+        /// <summary>
+        /// A data validation error.
+        /// </summary>
+        DataValidationError = 4 | HasError,
+
+        /// <summary>
+        /// A binding error with a fallback value.
+        /// </summary>
+        BindingErrorWithFallback = BindingError | HasValue,
+
+        /// <summary>
+        /// A data validation error with a fallback value.
+        /// </summary>
+        DataValidationErrorWithFallback = DataValidationError | HasValue,
+
+        TypeMask = 0x00ff,
+        HasValue = 0x0100,
+        HasError = 0x0200,
+    }
+
+    /// <summary>
+    /// A value passed into a binding.
+    /// </summary>
+    /// <typeparam name="T">The value type.</typeparam>
+    /// <remarks>
+    /// The avalonia binding system is typed, and as such additional state is stored in this
+    /// structure. A binding value can be in a number of states, described by the
+    /// <see cref="Type"/> property:
+    /// 
+    /// - <see cref="BindingValueType.Value"/>: a simple value
+    /// - <see cref="BindingValueType.UnsetValue"/>: the target property will revert to its unbound
+    ///   state until a new binding value is produced. Represented by
+    ///   <see cref="AvaloniaProperty.UnsetValue"/> in an untyped context
+    /// - <see cref="BindingValueType.DoNothing"/>: the binding value will be ignored. Represented
+    ///   by <see cref="BindingOperations.DoNothing"/> in an untyped context
+    /// - <see cref="BindingValueType.BindingError"/>: a binding error, such as a missing source
+    ///   property, with an optional fallback value
+    /// - <see cref="BindingValueType.DataValidationError"/>: a data validation error, with an
+    ///   optional fallback value
+    ///   
+    /// To create a new binding value you can:
+    /// 
+    /// - For a simple value, call the <see cref="BindingValue{T}"/> constructor or use an implicit
+    ///   conversion from <typeparamref name="T"/>
+    /// - For an unset value, use <see cref="Unset"/> or simply `default`
+    /// - For other types, call one of the static factory methods
+    /// </remarks>
+    public readonly struct BindingValue<T>
+    {
+        private readonly T _value;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BindingValue{T}"/> struct with a type of
+        /// <see cref="BindingValueType.Value"/>
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public BindingValue(T value)
+        {
+            ValidateValue(value);
+            _value = value;
+            Type = BindingValueType.Value;
+            Error = null;
+        }
+
+        private BindingValue(BindingValueType type, T value, Exception? error)
+        {
+            _value = value;
+            Type = type;
+            Error = error;
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the binding value represents either a binding or data
+        /// validation error.
+        /// </summary>
+        public bool HasError => Type.HasFlagCustom(BindingValueType.HasError);
+
+        /// <summary>
+        /// Gets a value indicating whether the binding value has a value.
+        /// </summary>
+        public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue);
+
+        /// <summary>
+        /// Gets the type of the binding value.
+        /// </summary>
+        public BindingValueType Type { get; }
+
+        /// <summary>
+        /// Gets the binding value or fallback value.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// <see cref="HasValue"/> is false.
+        /// </exception>
+        public T Value => HasValue ? _value : throw new InvalidOperationException("BindingValue has no value.");
+
+        /// <summary>
+        /// Gets the binding or data validation error.
+        /// </summary>
+        public Exception? Error { get; }
+
+        /// <summary>
+        /// Converts the binding value to an <see cref="Optional{T}"/>.
+        /// </summary>
+        /// <returns></returns>
+        public Optional<T> ToOptional() => HasValue ? new Optional<T>(Value) : default;
+
+        /// <inheritdoc/>
+        public override string ToString() => HasError ? $"Error: {Error!.Message}" : Value?.ToString() ?? "(null)";
+
+        /// <summary>
+        /// Converts the value to untyped representation, using <see cref="AvaloniaProperty.UnsetValue"/>,
+        /// <see cref="BindingOperations.DoNothing"/> and <see cref="BindingNotification"/> where
+        /// appropriate.
+        /// </summary>
+        /// <returns>The untyped representation of the binding value.</returns>
+        public object? ToUntyped()
+        {
+            return Type switch
+            {
+                BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue,
+                BindingValueType.DoNothing => BindingOperations.DoNothing,
+                BindingValueType.Value => Value,
+                BindingValueType.BindingError => 
+                    new BindingNotification(Error, BindingErrorType.Error),
+                BindingValueType.BindingErrorWithFallback =>
+                    new BindingNotification(Error, BindingErrorType.Error, Value),
+                BindingValueType.DataValidationError =>
+                    new BindingNotification(Error, BindingErrorType.DataValidationError),
+                BindingValueType.DataValidationErrorWithFallback =>
+                    new BindingNotification(Error, BindingErrorType.DataValidationError, Value),
+                _ => throw new NotSupportedException("Invalida BindingValueType."),
+            };
+        }
+
+        /// <summary>
+        /// Returns a new binding value with the specified value.
+        /// </summary>
+        /// <param name="value">The new value.</param>
+        /// <returns>The new binding value.</returns>
+        /// <exception cref="InvalidOperationException">
+        /// The binding type is <see cref="BindingValueType.UnsetValue"/> or
+        /// <see cref="BindingValueType.DoNothing"/>.
+        /// </exception>
+        public BindingValue<T> WithValue(T value)
+        {
+            if (Type == BindingValueType.DoNothing)
+            {
+                throw new InvalidOperationException("Cannot add value to DoNothing binding value.");
+            }
+
+            var type = Type == BindingValueType.UnsetValue ? BindingValueType.Value : Type;
+            return new BindingValue<T>(type | BindingValueType.HasValue, value, Error);
+        }
+
+        /// <summary>
+        /// Gets the value of the binding value if present, otherwise a default value.
+        /// </summary>
+        /// <param name="defaultValue">The default value.</param>
+        /// <returns>The value.</returns>
+        public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue;
+
+        /// <summary>
+        /// Gets the value of the binding value if present, otherwise a default value.
+        /// </summary>
+        /// <param name="defaultValue">The default value.</param>
+        /// <returns>
+        /// The value if present and of the correct type, `default(TResult)` if the value is
+        /// present but not of the correct type or null, or <paramref name="defaultValue"/> if the
+        /// value is not present.
+        /// </returns>
+        public TResult ValueOrDefault<TResult>(TResult defaultValue = default)
+        {
+            return HasValue ?
+                Value is TResult result ? result : default
+                : defaultValue;
+        }
+
+        /// <summary>
+        /// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
+        /// <see cref="AvaloniaProperty.UnsetValue"/> and <see cref="BindingOperations.DoNothing"/>.
+        /// </summary>
+        /// <param name="value">The untyped value.</param>
+        /// <returns>The typed binding value.</returns>
+        public static BindingValue<T> FromUntyped(object? value)
+        {
+            return value switch
+            {
+                UnsetValueType _ => Unset,
+                DoNothingType _ => DoNothing,
+                BindingNotification n => n.ToBindingValue().Cast<T>(),
+                _ => (T)value
+            };
+        }
+
+        /// <summary>
+        /// Creates a binding value from an instance of the underlying value type.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public static implicit operator BindingValue<T>(T value) => new BindingValue<T>(value);
+
+        /// <summary>
+        /// Creates a binding value from an <see cref="Optional{T}"/>.
+        /// </summary>
+        /// <param name="optional">The optional value.</param>
+
+        public static implicit operator BindingValue<T>(Optional<T> optional)
+        {
+            return optional.HasValue ? optional.Value : Unset;
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.UnsetValue"/>.
+        /// </summary>
+        public static BindingValue<T> Unset => new BindingValue<T>(BindingValueType.UnsetValue, default, null);
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.DoNothing"/>.
+        /// </summary>
+        public static BindingValue<T> DoNothing => new BindingValue<T>(BindingValueType.DoNothing, default, null);
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.BindingError"/>.
+        /// </summary>
+        /// <param name="e">The binding error.</param>
+        public static BindingValue<T> BindingError(Exception e)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(BindingValueType.BindingError, default, e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.BindingErrorWithFallback"/>.
+        /// </summary>
+        /// <param name="e">The binding error.</param>
+        /// <param name="fallbackValue">The fallback value.</param>
+        public static BindingValue<T> BindingError(Exception e, T fallbackValue)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(BindingValueType.BindingErrorWithFallback, fallbackValue, e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.BindingError"/> or
+        /// <see cref="BindingValueType.BindingErrorWithFallback"/>.
+        /// </summary>
+        /// <param name="e">The binding error.</param>
+        /// <param name="fallbackValue">The fallback value.</param>
+        public static BindingValue<T> BindingError(Exception e, Optional<T> fallbackValue)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(
+                fallbackValue.HasValue ?
+                    BindingValueType.BindingErrorWithFallback :
+                    BindingValueType.BindingError,
+                fallbackValue.HasValue ? fallbackValue.Value : default,
+                e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.DataValidationError"/>.
+        /// </summary>
+        /// <param name="e">The data validation error.</param>
+        public static BindingValue<T> DataValidationError(Exception e)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(BindingValueType.DataValidationError, default, e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.DataValidationErrorWithFallback"/>.
+        /// </summary>
+        /// <param name="e">The data validation error.</param>
+        /// <param name="fallbackValue">The fallback value.</param>
+        public static BindingValue<T> DataValidationError(Exception e, T fallbackValue)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.DataValidationError"/> or
+        /// <see cref="BindingValueType.DataValidationErrorWithFallback"/>.
+        /// </summary>
+        /// <param name="e">The binding error.</param>
+        /// <param name="fallbackValue">The fallback value.</param>
+        public static BindingValue<T> DataValidationError(Exception e, Optional<T> fallbackValue)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(
+                fallbackValue.HasValue ?
+                    BindingValueType.DataValidationError :
+                    BindingValueType.DataValidationErrorWithFallback,
+                fallbackValue.HasValue ? fallbackValue.Value : default,
+                e);
+        }
+
+        private static void ValidateValue(T value)
+        {
+            if (value is UnsetValueType)
+            {
+                throw new InvalidOperationException("AvaloniaValue.UnsetValue is not a valid value for BindingValue<>.");
+            }
+
+            if (value is DoNothingType)
+            {
+                throw new InvalidOperationException("BindingOperations.DoNothing is not a valid value for BindingValue<>.");
+            }
+        }
+    }
+
+    public static class BindingValueExtensions
+    {
+        /// <summary>
+        /// Casts the type of a <see cref="BindingValue{T}"/> using only the C# cast operator.
+        /// </summary>
+        /// <typeparam name="T">The target type.</typeparam>
+        /// <param name="value">The binding value.</param>
+        /// <returns>The cast value.</returns>
+        public static BindingValue<T> Cast<T>(this BindingValue<object> value)
+        {
+            return value.Type switch
+            {
+                BindingValueType.DoNothing => BindingValue<T>.DoNothing,
+                BindingValueType.UnsetValue => BindingValue<T>.Unset,
+                BindingValueType.Value => new BindingValue<T>((T)value.Value),
+                BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!),
+                BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError(
+                        value.Error!,
+                        (T)value.Value),
+                BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!),
+                BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError(
+                        value.Error!,
+                        (T)value.Value),
+                _ => throw new NotSupportedException("Invalid BindingValue type."),
+            };
+        }
+
+        /// <summary>
+        /// Casts the type of a <see cref="BindingValue{T}"/> using the implicit conversions
+        /// allowed by the C# language.
+        /// </summary>
+        /// <typeparam name="T">The target type.</typeparam>
+        /// <param name="value">The binding value.</param>
+        /// <returns>The cast value.</returns>
+        /// <remarks>
+        /// Note that this method uses reflection and as such may be slow.
+        /// </remarks>
+        public static BindingValue<T> Convert<T>(this BindingValue<object> value)
+        {
+            return value.Type switch
+            {
+                BindingValueType.DoNothing => BindingValue<T>.DoNothing,
+                BindingValueType.UnsetValue => BindingValue<T>.Unset,
+                BindingValueType.Value => new BindingValue<T>(TypeUtilities.ConvertImplicit<T>(value.Value)),
+                BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!),
+                BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError(
+                        value.Error!,
+                        TypeUtilities.ConvertImplicit<T>(value.Value)),
+                BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!),
+                BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError(
+                        value.Error!,
+                        TypeUtilities.ConvertImplicit<T>(value.Value)),
+                _ => throw new NotSupportedException("Invalid BindingValue type."),
+            };
+        }
+    }
+}

+ 129 - 0
src/Avalonia.Base/Data/Optional.cs

@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+
+#nullable enable
+
+namespace Avalonia.Data
+{
+    /// <summary>
+    /// An optional typed value.
+    /// </summary>
+    /// <typeparam name="T">The value type.</typeparam>
+    /// <remarks>
+    /// This struct is similar to <see cref="Nullable{T}"/> except it also accepts reference types:
+    /// note that null is a valid value for reference types. It is also similar to
+    /// <see cref="BindingValue{T}"/> but has only two states: "value present" and "value missing".
+    /// 
+    /// To create a new optional value you can:
+    /// 
+    /// - For a simple value, call the <see cref="Optional{T}"/> constructor or use an implicit
+    ///   conversion from <typeparamref name="T"/>
+    /// - For an missing value, use <see cref="Empty"/> or simply `default`
+    /// </remarks>
+    public struct Optional<T>
+    {
+        private readonly T _value;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Optional{T}"/> struct with value.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public Optional(T value)
+        {
+            _value = value;
+            HasValue = true;
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether a value is present.
+        /// </summary>
+        public bool HasValue { get; }
+
+        /// <summary>
+        /// Gets the value.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// <see cref="HasValue"/> is false.
+        /// </exception>
+        public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value.");
+
+        /// <inheritdoc/>
+        public override bool Equals(object obj) => obj is Optional<T> o && this == o;
+
+        /// <inheritdoc/>
+        public override int GetHashCode() => HasValue ? Value!.GetHashCode() : 0;
+
+        /// <summary>
+        /// Casts the value (if any) to an <see cref="object"/>.
+        /// </summary>
+        /// <returns>The cast optional value.</returns>
+        public Optional<object> ToObject() => HasValue ? new Optional<object>(Value) : default;
+
+        /// <inheritdoc/>
+        public override string ToString() => HasValue ? Value?.ToString() ?? "(null)" : "(empty)";
+
+        /// <summary>
+        /// Gets the value if present, otherwise a default value.
+        /// </summary>
+        /// <param name="defaultValue">The default value.</param>
+        /// <returns>The value.</returns>
+        public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue;
+
+        /// <summary>
+        /// Gets the value if present, otherwise a default value.
+        /// </summary>
+        /// <param name="defaultValue">The default value.</param>
+        /// <returns>
+        /// The value if present and of the correct type, `default(TResult)` if the value is
+        /// present but not of the correct type or null, or <paramref name="defaultValue"/> if the
+        /// value is not present.
+        /// </returns>
+        public TResult ValueOrDefault<TResult>(TResult defaultValue = default)
+        {
+            return HasValue ?
+                Value is TResult result ? result : default
+                : defaultValue;
+        }
+
+        /// <summary>
+        /// Creates an <see cref="Optional{T}"/> from an instance of the underlying value type.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public static implicit operator Optional<T>(T value) => new Optional<T>(value);
+
+        /// <summary>
+        /// Compares two <see cref="Optional{T}"/>s for inequality.
+        /// </summary>
+        /// <param name="x">The first value.</param>
+        /// <param name="y">The second value.</param>
+        /// <returns>True if the values are unequal; otherwise false.</returns>
+        public static bool operator !=(Optional<T> x, Optional<T> y) => !(x == y);
+
+        /// <summary>
+        /// Compares two <see cref="Optional{T}"/>s for equality.
+        /// </summary>
+        /// <param name="x">The first value.</param>
+        /// <param name="y">The second value.</param>
+        /// <returns>True if the values are equal; otherwise false.</returns>
+        public static bool operator==(Optional<T> x, Optional<T> y)
+        {
+            if (!x.HasValue && !y.HasValue)
+            {
+                return true;
+            }
+            else if (x.HasValue && y.HasValue)
+            {
+                return EqualityComparer<T>.Default.Equals(x.Value, y.Value);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Returns an <see cref="Optional{T}"/> without a value.
+        /// </summary>
+        public static Optional<T> Empty => default;
+    }
+}

+ 30 - 28
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
 using Avalonia.Data;
 
 namespace Avalonia.Diagnostics
@@ -21,35 +22,36 @@ namespace Avalonia.Diagnostics
         /// </returns>
         public static AvaloniaPropertyValue GetDiagnostic(this AvaloniaObject o, AvaloniaProperty property)
         {
-            var set = o.GetSetValues();
+            throw new NotImplementedException();
+            ////var set = o.GetSetValues();
 
-            if (set.TryGetValue(property, out var obj))
-            {
-                if (obj is PriorityValue value)
-                {
-                    return new AvaloniaPropertyValue(
-                        property,
-                        o.GetValue(property),
-                        (BindingPriority)value.ValuePriority,
-                        value.GetDiagnostic());
-                }
-                else
-                {
-                    return new AvaloniaPropertyValue(
-                        property,
-                        obj,
-                        BindingPriority.LocalValue,
-                        "Local value");
-                }
-            }
-            else
-            {
-                return new AvaloniaPropertyValue(
-                    property,
-                    o.GetValue(property),
-                    BindingPriority.Unset,
-                    "Unset");
-            }
+            ////if (set.TryGetValue(property, out var obj))
+            ////{
+            ////    if (obj is PriorityValue value)
+            ////    {
+            ////        return new AvaloniaPropertyValue(
+            ////            property,
+            ////            o.GetValue(property),
+            ////            (BindingPriority)value.ValuePriority,
+            ////            value.GetDiagnostic());
+            ////    }
+            ////    else
+            ////    {
+            ////        return new AvaloniaPropertyValue(
+            ////            property,
+            ////            obj,
+            ////            BindingPriority.LocalValue,
+            ////            "Local value");
+            ////    }
+            ////}
+            ////else
+            ////{
+            ////    return new AvaloniaPropertyValue(
+            ////        property,
+            ////        o.GetValue(property),
+            ////        BindingPriority.Unset,
+            ////        "Unset");
+            ////}
         }
     }
 }

+ 88 - 10
src/Avalonia.Base/DirectProperty.cs

@@ -16,7 +16,7 @@ namespace Avalonia
     /// <see cref="AvaloniaProperty"/> system. They hold a getter and an optional setter which
     /// allows the avalonia property system to read and write the current value.
     /// </remarks>
-    public class DirectProperty<TOwner, TValue> : AvaloniaProperty<TValue>, IDirectPropertyAccessor
+    public class DirectProperty<TOwner, TValue> : DirectPropertyBase<TValue>, IDirectPropertyAccessor
         where TOwner : IAvaloniaObject
     {
         /// <summary>
@@ -26,12 +26,16 @@ namespace Avalonia
         /// <param name="getter">Gets the current value of the property.</param>
         /// <param name="setter">Sets the value of the property. May be null.</param>
         /// <param name="metadata">The property metadata.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         public DirectProperty(
             string name,
             Func<TOwner, TValue> getter,
             Action<TOwner, TValue> setter,
-            DirectPropertyMetadata<TValue> metadata)
-            : base(name, typeof(TOwner), metadata)
+            DirectPropertyMetadata<TValue> metadata,
+            bool enableDataValidation)
+            : base(name, typeof(TOwner), metadata, enableDataValidation)
         {
             Contract.Requires<ArgumentNullException>(getter != null);
 
@@ -46,12 +50,16 @@ namespace Avalonia
         /// <param name="getter">Gets the current value of the property.</param>
         /// <param name="setter">Sets the value of the property. May be null.</param>
         /// <param name="metadata">Optional overridden metadata.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         private DirectProperty(
-            AvaloniaProperty<TValue> source,
+            DirectPropertyBase<TValue> source,
             Func<TOwner, TValue> getter,
             Action<TOwner, TValue> setter,
-            DirectPropertyMetadata<TValue> metadata)
-            : base(source, typeof(TOwner), metadata)
+            DirectPropertyMetadata<TValue> metadata,
+            bool enableDataValidation)
+            : base(source, typeof(TOwner), metadata, enableDataValidation)
         {
             Contract.Requires<ArgumentNullException>(getter != null);
 
@@ -65,6 +73,9 @@ namespace Avalonia
         /// <inheritdoc/>
         public override bool IsReadOnly => Setter == null;
 
+        /// <inheritdoc/>
+        public override Type Owner => typeof(TOwner);
+
         /// <summary>
         /// Gets the getter function.
         /// </summary>
@@ -75,9 +86,6 @@ namespace Avalonia
         /// </summary>
         public Action<TOwner, TValue> Setter { get; }
 
-        /// <inheritdoc/>
-        Type IDirectPropertyAccessor.Owner => typeof(TOwner);
-
         /// <summary>
         /// Registers the direct property on another type.
         /// </summary>
@@ -99,6 +107,45 @@ namespace Avalonia
             BindingMode defaultBindingMode = BindingMode.Default,
             bool enableDataValidation = false)
                 where TNewOwner : AvaloniaObject
+        {
+            var metadata = new DirectPropertyMetadata<TValue>(
+                unsetValue: unsetValue,
+                defaultBindingMode: defaultBindingMode);
+
+            metadata.Merge(GetMetadata<TOwner>(), this);
+
+            var result = new DirectProperty<TNewOwner, TValue>(
+                (DirectPropertyBase<TValue>)this,
+                getter,
+                setter,
+                metadata,
+                enableDataValidation);
+
+            AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
+            return result;
+        }
+
+        /// <summary>
+        /// Registers the direct property on another type.
+        /// </summary>
+        /// <typeparam name="TNewOwner">The type of the additional owner.</typeparam>
+        /// <param name="getter">Gets the current value of the property.</param>
+        /// <param name="setter">Sets the value of the property.</param>
+        /// <param name="unsetValue">
+        /// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
+        /// </param>
+        /// <param name="defaultBindingMode">The default binding mode for the property.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
+        /// <returns>The property.</returns>
+        public DirectProperty<TNewOwner, TValue> AddOwnerWithDataValidation<TNewOwner>(
+            Func<TNewOwner, TValue> getter,
+            Action<TNewOwner,TValue> setter,
+            TValue unsetValue = default(TValue),
+            BindingMode defaultBindingMode = BindingMode.Default,
+            bool enableDataValidation = false)
+                where TNewOwner : AvaloniaObject
         {
             var metadata = new DirectPropertyMetadata<TValue>(
                 unsetValue: unsetValue,
@@ -111,12 +158,33 @@ namespace Avalonia
                 this,
                 getter,
                 setter,
-                metadata);
+                metadata,
+                enableDataValidation);
 
             AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
             return result;
         }
 
+        /// <inheritdoc/>
+        internal override TValue InvokeGetter(IAvaloniaObject instance)
+        {
+            return Getter((TOwner)instance);
+        }
+
+        /// <inheritdoc/>
+        internal override void InvokeSetter(IAvaloniaObject instance, BindingValue<TValue> value)
+        {
+            if (Setter == null)
+            {
+                throw new ArgumentException($"The property {Name} is readonly.");
+            }
+
+            if (value.HasValue)
+            {
+                Setter((TOwner)instance, value.Value);
+            }
+        }
+
         /// <inheritdoc/>
         object IDirectPropertyAccessor.GetValue(IAvaloniaObject instance)
         {
@@ -133,5 +201,15 @@ namespace Avalonia
 
             Setter((TOwner)instance, (TValue)value);
         }
+
+        internal void WrapSetter(TOwner instance, BindingValue<TValue> value)
+        {
+            if (Setter == null)
+            {
+                throw new ArgumentException($"The property {Name} is readonly.");
+            }
+
+            Setter(instance, value.Value);
+        }
     }
 }

+ 159 - 0
src/Avalonia.Base/DirectPropertyBase.cs

@@ -0,0 +1,159 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Base class for direct properties.
+    /// </summary>
+    /// <typeparam name="TValue">The type of the property's value.</typeparam>
+    /// <remarks>
+    /// Whereas <see cref="DirectProperty{TOwner, TValue}"/> is typed on the owner type, this base
+    /// class provides a non-owner-typed interface to a direct poperty.
+    /// </remarks>
+    public abstract class DirectPropertyBase<TValue> : AvaloniaProperty<TValue>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DirectPropertyBase{TValue}"/> class.
+        /// </summary>
+        /// <param name="name">The name of the property.</param>
+        /// <param name="ownerType">The type of the class that registers the property.</param>
+        /// <param name="metadata">The property metadata.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
+        protected DirectPropertyBase(
+            string name,
+            Type ownerType,
+            PropertyMetadata metadata,
+            bool enableDataValidation)
+            : base(name, ownerType, metadata)
+        {
+            IsDataValidationEnabled = enableDataValidation;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
+        /// </summary>
+        /// <param name="source">The property to copy.</param>
+        /// <param name="ownerType">The new owner type.</param>
+        /// <param name="metadata">Optional overridden metadata.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
+        protected DirectPropertyBase(
+            AvaloniaProperty source,
+            Type ownerType,
+            PropertyMetadata metadata,
+            bool enableDataValidation)
+            : base(source, ownerType, metadata)
+        {
+            IsDataValidationEnabled = enableDataValidation;
+        }
+
+        /// <summary>
+        /// Gets the type that registered the property.
+        /// </summary>
+        public abstract Type Owner { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether data validation is enabled for the property.
+        /// </summary>
+        public bool IsDataValidationEnabled { get; }
+
+        /// <summary>
+        /// Gets the value of the property on the instance.
+        /// </summary>
+        /// <param name="instance">The instance.</param>
+        /// <returns>The property value.</returns>
+        internal abstract TValue InvokeGetter(IAvaloniaObject instance);
+
+        /// <summary>
+        /// Sets the value of the property on the instance.
+        /// </summary>
+        /// <param name="instance">The instance.</param>
+        /// <param name="value">The value.</param>
+        internal abstract void InvokeSetter(IAvaloniaObject instance, BindingValue<TValue> value);
+
+        /// <summary>
+        /// Gets the unset value for the property on the specified type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>The unset value.</returns>
+        public TValue GetUnsetValue(Type type)
+        {
+            type = type ?? throw new ArgumentNullException(nameof(type));
+            return GetMetadata(type).UnsetValue;
+        }
+
+        /// <summary>
+        /// Gets the property metadata for the specified type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>
+        /// The property metadata.
+        /// </returns>
+        public new DirectPropertyMetadata<TValue> GetMetadata(Type type)
+        {
+            return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
+        }
+
+        /// <inheritdoc/>
+        internal override void NotifyInitialized(IAvaloniaObject o)
+        {
+            var e = new AvaloniaPropertyChangedEventArgs<TValue>(
+                o,
+                this,
+                default,
+                InvokeGetter(o),
+                BindingPriority.Unset);
+            NotifyInitialized(e);
+        }
+
+        /// <inheritdoc/>
+        internal override object? RouteGetValue(IAvaloniaObject o)
+        {
+            return o.GetValue<TValue>(this);
+        }
+
+        /// <inheritdoc/>
+        internal override void RouteSetValue(
+            IAvaloniaObject o,
+            object value,
+            BindingPriority priority)
+        {
+            var v = TryConvert(value);
+
+            if (v.HasValue)
+            {
+                o.SetValue<TValue>(this, (TValue)v.Value, priority);
+            }
+            else if (v.Type == BindingValueType.UnsetValue)
+            {
+                o.ClearValue(this);
+            }
+            else if (v.HasError)
+            {
+                throw v.Error!;
+            }
+        }
+
+        /// <inheritdoc/>
+        internal override IDisposable RouteBind(
+            IAvaloniaObject o,
+            IObservable<BindingValue<object>> source,
+            BindingPriority priority)
+        {
+            var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
+            return o.Bind<TValue>(this, adapter, priority);
+        }
+
+        internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent)
+        {
+            throw new NotSupportedException("Direct properties do not support inheritance.");
+        }
+    }
+}

+ 50 - 4
src/Avalonia.Base/IAvaloniaObject.cs

@@ -17,9 +17,16 @@ namespace Avalonia
         event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
 
         /// <summary>
-        /// Raised when an inheritable <see cref="AvaloniaProperty"/> value changes on this object.
+        /// Clears a <see cref="AvaloniaProperty"/>'s local value.
         /// </summary>
-        event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
+        /// <param name="property">The property.</param>
+        public void ClearValue(AvaloniaProperty property);
+
+        /// <summary>
+        /// Clears a <see cref="AvaloniaProperty"/>'s local value.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        public void ClearValue<T>(AvaloniaProperty<T> property);
 
         /// <summary>
         /// Gets a <see cref="AvaloniaProperty"/> value.
@@ -84,7 +91,7 @@ namespace Avalonia
         /// </returns>
         IDisposable Bind(
             AvaloniaProperty property,
-            IObservable<object> source,
+            IObservable<BindingValue<object>> source,
             BindingPriority priority = BindingPriority.LocalValue);
 
         /// <summary>
@@ -99,7 +106,46 @@ namespace Avalonia
         /// </returns>
         IDisposable Bind<T>(
             AvaloniaProperty<T> property,
-            IObservable<T> source,
+            IObservable<BindingValue<T>> source,
             BindingPriority priority = BindingPriority.LocalValue);
+
+        /// <summary>
+        /// Registers an object as an inheritance child.
+        /// </summary>
+        /// <param name="child">The inheritance child.</param>
+        /// <remarks>
+        /// Inheritance children will recieve a call to
+        /// <see cref="InheritedPropertyChanged{T}(AvaloniaProperty{T}, Optional{T}, Optional{T})"/>
+        /// when an inheritable property value changes on the parent.
+        /// </remarks>
+        void AddInheritanceChild(IAvaloniaObject child);
+
+        /// <summary>
+        /// Unregisters an object as an inheritance child.
+        /// </summary>
+        /// <param name="child">The inheritance child.</param>
+        /// <remarks>
+        /// Removes an inheritance child that was added by a call to
+        /// <see cref="AddInheritanceChild(IAvaloniaObject)"/>.
+        /// </remarks>
+        void RemoveInheritanceChild(IAvaloniaObject child);
+
+        //void InheritanceParentChanged<T>(
+        //    StyledPropertyBase<T> property,
+        //    IAvaloniaObject oldParent,
+        //    IAvaloniaObject newParent);
+
+        /// <summary>
+        /// Called when an inheritable property changes on an object registered as an inheritance
+        /// parent.
+        /// </summary>
+        /// <typeparam name="T">The type of the value.</typeparam>
+        /// <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>
+        void InheritedPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            Optional<T> newValue);
     }
 }

+ 0 - 51
src/Avalonia.Base/IPriorityValueOwner.cs

@@ -1,51 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using Avalonia.Data;
-using Avalonia.Utilities;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// An owner of a <see cref="PriorityValue"/>.
-    /// </summary>
-    internal interface IPriorityValueOwner
-    {
-        /// <summary>
-        /// Called when a <see cref="PriorityValue"/>'s value changes.
-        /// </summary>
-        /// <param name="property">The the property that has changed.</param>
-        /// <param name="priority">The priority of the value.</param>
-        /// <param name="oldValue">The old value.</param>
-        /// <param name="newValue">The new value.</param>
-        void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue);
-
-        /// <summary>
-        /// Called when a <see cref="BindingNotification"/> is received by a 
-        /// <see cref="PriorityValue"/>.
-        /// </summary>
-        /// <param name="property">The the property that has changed.</param>
-        /// <param name="notification">The notification.</param>
-        void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
-
-        /// <summary>
-        /// Returns deferred setter for given non-direct property.
-        /// </summary>
-        /// <param name="property">Property.</param>
-        /// <returns>Deferred setter for given property.</returns>
-        DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property);
-
-        /// <summary>
-        /// Logs a binding error.
-        /// </summary>
-        /// <param name="property">The property the error occurred on.</param>
-        /// <param name="e">The binding error.</param>
-        void LogError(AvaloniaProperty property, Exception e);
-
-        /// <summary>
-        /// Ensures that the current thread is the UI thread.
-        /// </summary>
-        void VerifyAccess();
-    }
-}

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

@@ -18,14 +18,5 @@ namespace Avalonia
         /// The default value.
         /// </returns>
         object GetDefaultValue(Type type);
-
-        /// <summary>
-        /// Gets a validation function for the property on the specified type.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <returns>
-        /// The validation function, or null if no validation function exists.
-        /// </returns>
-        Func<IAvaloniaObject, object, object> GetValidationFunc(Type type);
     }
 }

+ 1 - 6
src/Avalonia.Base/IStyledPropertyMetadata.cs

@@ -14,10 +14,5 @@ namespace Avalonia
         /// Gets the default value for the property.
         /// </summary>
         object DefaultValue { get; }
-
-        /// <summary>
-        /// Gets the property's validation function.
-        /// </summary>
-        Func<IAvaloniaObject, object, object> Validate { get; }
     }
-}
+}

+ 0 - 160
src/Avalonia.Base/PriorityBindingEntry.cs

@@ -1,160 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Runtime.ExceptionServices;
-using Avalonia.Data;
-using Avalonia.Threading;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// A registered binding in a <see cref="PriorityValue"/>.
-    /// </summary>
-    internal class PriorityBindingEntry : IDisposable, IObserver<object>
-    {
-        private readonly PriorityLevel _owner;
-        private IDisposable _subscription;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PriorityBindingEntry"/> class.
-        /// </summary>
-        /// <param name="owner">The owner.</param>
-        /// <param name="index">
-        /// The binding index. Later bindings should have higher indexes.
-        /// </param>
-        public PriorityBindingEntry(PriorityLevel owner, int index)
-        {
-            _owner = owner;
-            Index = index;
-        }
-
-        /// <summary>
-        /// Gets the observable associated with the entry.
-        /// </summary>
-        public IObservable<object> Observable { get; private set; }
-
-        /// <summary>
-        /// Gets a description of the binding.
-        /// </summary>
-        public string Description
-        {
-            get;
-            private set;
-        }
-
-        /// <summary>
-        /// Gets the binding entry index. Later bindings will have higher indexes.
-        /// </summary>
-        public int Index
-        {
-            get;
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether the binding has completed.
-        /// </summary>
-        public bool HasCompleted { get; private set; }
-
-        /// <summary>
-        /// The current value of the binding.
-        /// </summary>
-        public object Value
-        {
-            get;
-            private set;
-        }
-
-        /// <summary>
-        /// Starts listening to the binding.
-        /// </summary>
-        /// <param name="binding">The binding.</param>
-        public void Start(IObservable<object> binding)
-        {
-            Contract.Requires<ArgumentNullException>(binding != null);
-
-            if (_subscription != null)
-            {
-                throw new Exception("PriorityValue.Entry.Start() called more than once.");
-            }
-
-            Observable = binding;
-            Value = AvaloniaProperty.UnsetValue;
-
-            if (binding is IDescription)
-            {
-                Description = ((IDescription)binding).Description;
-            }
-
-            _subscription = binding.Subscribe(this);
-        }
-
-        /// <summary>
-        /// Ends the binding subscription.
-        /// </summary>
-        public void Dispose()
-        {
-            _subscription?.Dispose();
-        }
-
-        void IObserver<object>.OnNext(object value)
-        {
-            void Signal(PriorityBindingEntry instance, object newValue)
-            {
-                var notification = newValue as BindingNotification;
-
-                if (notification != null)
-                {
-                    if (notification.HasValue || notification.ErrorType == BindingErrorType.Error)
-                    {
-                        instance.Value = notification.Value;
-                        instance._owner.Changed(instance);
-                    }
-
-                    if (notification.ErrorType != BindingErrorType.None)
-                    {
-                        instance._owner.Error(instance, notification);
-                    }
-                }
-                else
-                {
-                    instance.Value = newValue;
-                    instance._owner.Changed(instance);
-                }
-            }
-
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                Signal(this, 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;
-
-                Dispatcher.UIThread.Post(() => Signal(instance, newValue));
-            }
-        }
-
-        void IObserver<object>.OnCompleted()
-        {
-            HasCompleted = true;
-
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                _owner.Completed(this);
-            }
-            else
-            {
-                Dispatcher.UIThread.Post(() => _owner.Completed(this));
-            }
-        }
-
-        void IObserver<object>.OnError(Exception error)
-        {
-            ExceptionDispatchInfo.Capture(error).Throw();
-        }
-    }
-}

+ 0 - 227
src/Avalonia.Base/PriorityLevel.cs

@@ -1,227 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Threading;
-using Avalonia.Data;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// Stores bindings for a priority level in a <see cref="PriorityValue"/>.
-    /// </summary>
-    /// <remarks>
-    /// <para>
-    /// Each priority level in a <see cref="PriorityValue"/> has a current <see cref="Value"/>,
-    /// a list of <see cref="Bindings"/> and a <see cref="DirectValue"/>. When there are no
-    /// bindings present, or all bindings return <see cref="AvaloniaProperty.UnsetValue"/> then
-    /// <code>Value</code> will equal <code>DirectValue</code>.
-    /// </para>
-    /// <para>
-    /// When there are bindings present, then the latest added binding that doesn't return
-    /// <code>UnsetValue</code> will take precedence. The active binding is returned by the
-    /// <see cref="ActiveBindingIndex"/> property (which refers to the active binding's
-    /// <see cref="PriorityBindingEntry.Index"/> property rather than the index in
-    /// <code>Bindings</code>).
-    /// </para>
-    /// <para>
-    /// If <code>DirectValue</code> is set while a binding is active, then it will replace the
-    /// current value until the active binding fires again.
-    /// </para>
-    /// </remarks>
-    internal class PriorityLevel
-    {
-        private object _directValue;
-        private int _nextIndex;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PriorityLevel"/> class.
-        /// </summary>
-        /// <param name="owner">The owner.</param>
-        /// <param name="priority">The priority.</param>
-        public PriorityLevel(
-            PriorityValue owner,
-            int priority)
-        {
-            Contract.Requires<ArgumentNullException>(owner != null);
-
-            Owner = owner;
-            Priority = priority;
-            Value = _directValue = AvaloniaProperty.UnsetValue;
-            ActiveBindingIndex = -1;
-            Bindings = new LinkedList<PriorityBindingEntry>();
-        }
-
-        /// <summary>
-        /// Gets the owner of the level.
-        /// </summary>
-        public PriorityValue Owner { get; }
-
-        /// <summary>
-        /// Gets the priority of this level.
-        /// </summary>
-        public int Priority { get; }
-
-        /// <summary>
-        /// Gets or sets the direct value for this priority level.
-        /// </summary>
-        public object DirectValue
-        {
-            get
-            {
-                return _directValue;
-            }
-
-            set
-            {
-                Value = _directValue = value;
-                Owner.LevelValueChanged(this);
-            }
-        }
-
-        /// <summary>
-        /// Gets the current binding for the priority level.
-        /// </summary>
-        public object Value { get; private set; }
-
-        /// <summary>
-        /// Gets the <see cref="PriorityBindingEntry.Index"/> value of the active binding, or -1
-        /// if no binding is active.
-        /// </summary>
-        public int ActiveBindingIndex { get; private set; }
-
-        /// <summary>
-        /// Gets the bindings for the priority level.
-        /// </summary>
-        public LinkedList<PriorityBindingEntry> Bindings { get; }
-
-        /// <summary>
-        /// Adds a binding.
-        /// </summary>
-        /// <param name="binding">The binding to add.</param>
-        /// <returns>A disposable used to remove the binding.</returns>
-        public IDisposable Add(IObservable<object> binding)
-        {
-            Contract.Requires<ArgumentNullException>(binding != null);
-
-            var entry = new PriorityBindingEntry(this, _nextIndex++);
-            var node = Bindings.AddFirst(entry);
-
-            entry.Start(binding);
-
-            return new RemoveBindingDisposable(node, Bindings, this);
-        }
-
-        /// <summary>
-        /// Invoked when an entry in <see cref="Bindings"/> changes value.
-        /// </summary>
-        /// <param name="entry">The entry that changed.</param>
-        public void Changed(PriorityBindingEntry entry)
-        {
-            if (entry.Index >= ActiveBindingIndex)
-            {
-                if (entry.Value != AvaloniaProperty.UnsetValue)
-                {
-                    Value = entry.Value;
-                    ActiveBindingIndex = entry.Index;
-                    Owner.LevelValueChanged(this);
-                }
-                else
-                {
-                    ActivateFirstBinding();
-                }
-            }
-        }
-
-        /// <summary>
-        /// Invoked when an entry in <see cref="Bindings"/> completes.
-        /// </summary>
-        /// <param name="entry">The entry that completed.</param>
-        public void Completed(PriorityBindingEntry entry)
-        {
-            Bindings.Remove(entry);
-
-            if (entry.Index >= ActiveBindingIndex)
-            {
-                ActivateFirstBinding();
-            }
-        }
-
-        /// <summary>
-        /// Invoked when an entry in <see cref="Bindings"/> encounters a recoverable error.
-        /// </summary>
-        /// <param name="entry">The entry that completed.</param>
-        /// <param name="error">The error.</param>
-        public void Error(PriorityBindingEntry entry, BindingNotification error)
-        {
-            Owner.LevelError(this, error);
-        }
-
-        /// <summary>
-        /// Activates the first binding that has a value.
-        /// </summary>
-        private void ActivateFirstBinding()
-        {
-            foreach (var binding in Bindings)
-            {
-                if (binding.Value != AvaloniaProperty.UnsetValue)
-                {
-                    Value = binding.Value;
-                    ActiveBindingIndex = binding.Index;
-                    Owner.LevelValueChanged(this);
-                    return;
-                }
-            }
-
-            Value = DirectValue;
-            ActiveBindingIndex = -1;
-            Owner.LevelValueChanged(this);
-        }
-
-        private sealed class RemoveBindingDisposable : IDisposable
-        {
-            private readonly LinkedList<PriorityBindingEntry> _bindings;
-            private readonly PriorityLevel _priorityLevel;
-            private LinkedListNode<PriorityBindingEntry> _binding;
-
-            public RemoveBindingDisposable(
-                LinkedListNode<PriorityBindingEntry> binding,
-                LinkedList<PriorityBindingEntry> bindings,
-                PriorityLevel priorityLevel)
-            {
-                _binding = binding;
-                _bindings = bindings;
-                _priorityLevel = priorityLevel;
-            }
-
-            public void Dispose()
-            {
-                LinkedListNode<PriorityBindingEntry> binding = Interlocked.Exchange(ref _binding, null);
-
-                if (binding == null)
-                {
-                    // Some system is trying to remove binding twice.
-                    Debug.Assert(false);
-
-                    return;
-                }
-
-                PriorityBindingEntry entry = binding.Value;
-
-                if (!entry.HasCompleted)
-                {
-                    _bindings.Remove(binding);
-
-                    entry.Dispose();
-
-                    if (entry.Index >= _priorityLevel.ActiveBindingIndex)
-                    {
-                        _priorityLevel.ActivateFirstBinding();
-                    }
-                }
-            }
-        }
-    }
-}

+ 0 - 315
src/Avalonia.Base/PriorityValue.cs

@@ -1,315 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Avalonia.Data;
-using Avalonia.Logging;
-using Avalonia.Utilities;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// Maintains a list of prioritized bindings together with a current value.
-    /// </summary>
-    /// <remarks>
-    /// Bindings, in the form of <see cref="IObservable{Object}"/>s are added to the object using
-    /// the <see cref="Add"/> method. With the observable is passed a priority, where lower values
-    /// represent higher priorities. The current <see cref="Value"/> is selected from the highest
-    /// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
-    /// are multiple bindings registered with the same priority, the most recently added binding
-    /// has a higher priority. Each time the value changes, the 
-    /// <see cref="IPriorityValueOwner.Changed"/> method on the 
-    /// owner object is fired with the old and new values.
-    /// </remarks>
-    internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)>
-    {
-        private readonly Type _valueType;
-        private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
-        private readonly Func<object, object> _validate;
-        private (object value, int priority) _value;
-        private DeferredSetter<object> _setter;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PriorityValue"/> class.
-        /// </summary>
-        /// <param name="owner">The owner of the object.</param>
-        /// <param name="property">The property that the value represents.</param>
-        /// <param name="valueType">The value type.</param>
-        /// <param name="validate">An optional validation function.</param>
-        public PriorityValue(
-            IPriorityValueOwner owner,
-            AvaloniaProperty property, 
-            Type valueType,
-            Func<object, object> validate = null)
-        {
-            Owner = owner;
-            Property = property;
-            _valueType = valueType;
-            _value = (AvaloniaProperty.UnsetValue, int.MaxValue);
-            _validate = validate;
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether the property is animating.
-        /// </summary>
-        public bool IsAnimating
-        {
-            get
-            {
-                return ValuePriority <= (int)BindingPriority.Animation && 
-                    GetLevel(ValuePriority).ActiveBindingIndex != -1;
-            }
-        }
-
-        /// <summary>
-        /// Gets the owner of the value.
-        /// </summary>
-        public IPriorityValueOwner Owner { get; }
-
-        /// <summary>
-        /// Gets the property that the value represents.
-        /// </summary>
-        public AvaloniaProperty Property { get; }
-
-        /// <summary>
-        /// Gets the current value.
-        /// </summary>
-        public object Value => _value.value;
-
-        /// <summary>
-        /// Gets the priority of the binding that is currently active.
-        /// </summary>
-        public int ValuePriority => _value.priority;
-
-        /// <summary>
-        /// Adds a new binding.
-        /// </summary>
-        /// <param name="binding">The binding.</param>
-        /// <param name="priority">The binding priority.</param>
-        /// <returns>
-        /// A disposable that will remove the binding.
-        /// </returns>
-        public IDisposable Add(IObservable<object> binding, int priority)
-        {
-            return GetLevel(priority).Add(binding);
-        }
-
-        /// <summary>
-        /// Sets the value for a specified priority.
-        /// </summary>
-        /// <param name="value">The value.</param>
-        /// <param name="priority">The priority</param>
-        public void SetValue(object value, int priority)
-        {
-            GetLevel(priority).DirectValue = value;
-        }
-
-        /// <summary>
-        /// Gets the currently active bindings on this object.
-        /// </summary>
-        /// <returns>An enumerable collection of bindings.</returns>
-        public IEnumerable<PriorityBindingEntry> GetBindings()
-        {
-            foreach (var level in _levels)
-            {
-                foreach (var binding in level.Value.Bindings)
-                {
-                    yield return binding;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Returns diagnostic string that can help the user debug the bindings in effect on
-        /// this object.
-        /// </summary>
-        /// <returns>A diagnostic string.</returns>
-        public string GetDiagnostic()
-        {
-            var b = new StringBuilder();
-            var first = true;
-
-            foreach (var level in _levels)
-            {
-                if (!first)
-                {
-                    b.AppendLine();
-                }
-
-                b.Append(ValuePriority == level.Key ? "*" : string.Empty);
-                b.Append("Priority ");
-                b.Append(level.Key);
-                b.Append(": ");
-                b.AppendLine(level.Value.Value?.ToString() ?? "(null)");
-                b.AppendLine("--------");
-                b.Append("Direct: ");
-                b.AppendLine(level.Value.DirectValue?.ToString() ?? "(null)");
-
-                foreach (var binding in level.Value.Bindings)
-                {
-                    b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty);
-                    b.Append(binding.Description ?? binding.Observable.GetType().Name);
-                    b.Append(": ");
-                    b.AppendLine(binding.Value?.ToString() ?? "(null)");
-                }
-
-                first = false;
-            }
-
-            return b.ToString();
-        }
-
-        /// <summary>
-        /// Called when the value for a priority level changes.
-        /// </summary>
-        /// <param name="level">The priority level of the changed entry.</param>
-        public void LevelValueChanged(PriorityLevel level)
-        {
-            if (level.Priority <= ValuePriority)
-            {
-                if (level.Value != AvaloniaProperty.UnsetValue)
-                {
-                    UpdateValue(level.Value, level.Priority);
-                }
-                else
-                {
-                    foreach (var i in _levels.Values.OrderBy(x => x.Priority))
-                    {
-                        if (i.Value != AvaloniaProperty.UnsetValue)
-                        {
-                            UpdateValue(i.Value, i.Priority);
-                            return;
-                        }
-                    }
-
-                    UpdateValue(AvaloniaProperty.UnsetValue, int.MaxValue);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Called when a priority level encounters an error.
-        /// </summary>
-        /// <param name="level">The priority level of the changed entry.</param>
-        /// <param name="error">The binding error.</param>
-        public void LevelError(PriorityLevel level, BindingNotification error)
-        {
-            Owner.LogError(Property, error.Error);
-        }
-
-        /// <summary>
-        /// Causes a revalidation of the value.
-        /// </summary>
-        public void Revalidate()
-        {
-            if (_validate != null)
-            {
-                PriorityLevel level;
-
-                if (_levels.TryGetValue(ValuePriority, out level))
-                {
-                    UpdateValue(level.Value, level.Priority);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets the <see cref="PriorityLevel"/> with the specified priority, creating it if it
-        /// doesn't already exist.
-        /// </summary>
-        /// <param name="priority">The priority.</param>
-        /// <returns>The priority level.</returns>
-        private PriorityLevel GetLevel(int priority)
-        {
-            PriorityLevel result;
-
-            if (!_levels.TryGetValue(priority, out result))
-            {
-                result = new PriorityLevel(this, priority);
-                _levels.Add(priority, result);
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// Updates the current <see cref="Value"/> and notifies all subscribers.
-        /// </summary>
-        /// <param name="value">The value to set.</param>
-        /// <param name="priority">The priority level that the value came from.</param>
-        private void UpdateValue(object value, int priority)
-        {
-            var newValue = (value, priority);
-
-            if (newValue == _value)
-            {
-                return;
-            }
-
-            if (_setter == null)
-            {
-                _setter = Owner.GetNonDirectDeferredSetter(Property);
-            }
-
-            _setter.SetAndNotifyCallback(Property, this, ref _value, newValue);
-        }
-
-        void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value)
-        {
-            SetAndNotify(ref backing, value);
-        }
-
-        private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update)
-        {
-            var val = update.value;
-            var notification = val as BindingNotification;
-            object castValue;
-
-            if (notification != null)
-            {
-                val = (notification.HasValue) ? notification.Value : null;
-            }
-
-            if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue))
-            {
-                var old = backing.value;
-
-                if (_validate != null && castValue != AvaloniaProperty.UnsetValue)
-                {
-                    castValue = _validate(castValue);
-                }
-
-                backing = (castValue, update.priority);
-
-                if (notification?.HasValue == true)
-                {
-                    notification.SetValue(castValue);
-                }
-
-                if (notification == null || notification.HasValue)
-                {
-                    Owner?.Changed(Property, ValuePriority, old, Value);
-                }
-
-                if (notification != null)
-                {
-                    Owner?.BindingNotificationReceived(Property, notification);
-                }
-            }
-            else
-            {
-                Logger.TryGet(LogEventLevel.Error)?.Log(
-                    LogArea.Binding,
-                    Owner,
-                    "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
-                    Property.Name,
-                    _valueType,
-                    val,
-                    val?.GetType());
-            }
-        }
-    }
-}

+ 95 - 0
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -0,0 +1,95 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Threading;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    internal interface IBindingEntry : IPriorityValueEntry, IDisposable
+    {
+    }
+
+    internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>>
+    {
+        private readonly IAvaloniaObject _owner;
+        private IValueSink _sink;
+        private IDisposable? _subscription;
+
+        public BindingEntry(
+            IAvaloniaObject owner,
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority,
+            IValueSink sink)
+        {
+            _owner = owner;
+            Property = property;
+            Source = source;
+            Priority = priority;
+            _sink = sink;
+        }
+
+        public StyledPropertyBase<T> Property { get; }
+        public BindingPriority Priority { get; }
+        public IObservable<BindingValue<T>> Source { get; }
+        public Optional<T> Value { get; private set; }
+        Optional<object> IValue.Value => Value.ToObject();
+        BindingPriority IValue.ValuePriority => Priority;
+
+        public void Dispose()
+        {
+            _subscription?.Dispose();
+            _subscription = null;
+            _sink.Completed(Property, this);
+        }
+
+        public void OnCompleted() => _sink.Completed(Property, this);
+
+        public void OnError(Exception error)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void OnNext(BindingValue<T> value)
+        {
+            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;
+
+                Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue));
+            }
+        }
+
+        public void Start()
+        {
+            _subscription = Source.Subscribe(this);
+        }
+
+        public void Reparent(IValueSink sink) => _sink = sink;
+        
+        private void UpdateValue(BindingValue<T> value)
+        {
+            if (value.Type == BindingValueType.DoNothing)
+            {
+                return;
+            }
+
+            var old = Value;
+
+            if (value.Type != BindingValueType.DataValidationError)
+            {
+                Value = value.ToOptional();
+            }
+
+            _sink.ValueChanged(Property, Priority, old, value);
+        }
+    }
+}

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

@@ -0,0 +1,28 @@
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>
+    {
+        public ConstantValueEntry(
+            StyledPropertyBase<T> property,
+            T value,
+            BindingPriority priority)
+        {
+            Property = property;
+            Value = value;
+            Priority = priority;
+        }
+
+        public StyledPropertyBase<T> Property { get; }
+        public BindingPriority Priority { get; }
+        public Optional<T> Value { get; private set; }
+        Optional<object> IValue.Value => Value.ToObject();
+        BindingPriority IValue.ValuePriority => Priority;
+
+        public void Reparent(IValueSink sink) { }
+    }
+}

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

@@ -0,0 +1,18 @@
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    internal interface IPriorityValueEntry : IValue
+    {
+        BindingPriority Priority { get; }
+
+        void Reparent(IValueSink sink);
+    }
+
+    internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T>
+    {
+    }
+}

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

@@ -0,0 +1,17 @@
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    internal interface IValue
+    {
+        Optional<object> Value { get; }
+        BindingPriority ValuePriority { get; }
+    }
+
+    internal interface IValue<T> : IValue
+    {
+        new Optional<T> Value { get; }
+    }
+}

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

@@ -0,0 +1,18 @@
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    internal interface IValueSink
+    {
+        void ValueChanged<T>(
+            StyledPropertyBase<T> property,
+            BindingPriority priority,
+            Optional<T> oldValue,
+            BindingValue<T> newValue);
+
+        void Completed(AvaloniaProperty property, IPriorityValueEntry entry);
+    }
+}

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

@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    internal class PriorityValue<T> : IValue<T>, IValueSink
+    {
+        private readonly IAvaloniaObject _owner;
+        private readonly IValueSink _sink;
+        private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
+        private Optional<T> _localValue;
+
+        public PriorityValue(
+            IAvaloniaObject owner,
+            StyledPropertyBase<T> property,
+            IValueSink sink)
+        {
+            _owner = owner;
+            Property = property;
+            _sink = sink;
+        }
+
+        public PriorityValue(
+            IAvaloniaObject owner,
+            StyledPropertyBase<T> property,
+            IValueSink sink,
+            IPriorityValueEntry<T> existing)
+            : this(owner, property, sink)
+        {
+            existing.Reparent(this);
+            _entries.Add(existing);
+            
+            if (existing.Value.HasValue)
+            {
+                Value = existing.Value;
+                ValuePriority = existing.Priority;
+            }
+        }
+
+        public StyledPropertyBase<T> Property { get; }
+        public Optional<T> Value { get; private set; }
+        public BindingPriority ValuePriority { get; private set; }
+        public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
+        Optional<object> IValue.Value => Value.ToObject();
+
+        public void ClearLocalValue() => UpdateEffectiveValue();
+
+        public void SetValue(T value, BindingPriority priority)
+        {
+            if (priority == BindingPriority.LocalValue)
+            {
+                _localValue = value;
+            }
+            else
+            {
+                var insert = FindInsertPoint(priority);
+                _entries.Insert(insert, new ConstantValueEntry<T>(Property, value, priority));
+            }
+
+            UpdateEffectiveValue();
+        }
+
+        public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)
+        {
+            var binding = new BindingEntry<T>(_owner, Property, source, priority, this);
+            var insert = FindInsertPoint(binding.Priority);
+            _entries.Insert(insert, binding);
+            return binding;
+        }
+
+        void IValueSink.ValueChanged<TValue>(
+            StyledPropertyBase<TValue> property,
+            BindingPriority priority,
+            Optional<TValue> oldValue,
+            BindingValue<TValue> newValue)
+        {
+            _localValue = default;
+            UpdateEffectiveValue();
+        }
+
+        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry)
+        {
+            _entries.Remove((IPriorityValueEntry<T>)entry);
+            UpdateEffectiveValue();
+        }
+
+        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;
+        }
+
+        private void UpdateEffectiveValue()
+        {
+            var reachedLocalValues = false;
+            var value = default(Optional<T>);
+
+            for (var i = _entries.Count - 1; i >= 0; --i)
+            {
+                var entry = _entries[i];
+
+                if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue)
+                {
+                    reachedLocalValues = true;
+
+                    if (_localValue.HasValue)
+                    {
+                        value = _localValue;
+                        ValuePriority = BindingPriority.LocalValue;
+                        break;
+                    }
+                }
+
+                if (entry.Value.HasValue)
+                {
+                    value = entry.Value;
+                    ValuePriority = entry.Priority;
+                    break;
+                }
+            }
+
+            if (value != Value)
+            {
+                var old = Value;
+                Value = value;
+                _sink.ValueChanged(Property, ValuePriority, old, value);
+            }
+        }
+    }
+}

+ 55 - 0
src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs

@@ -0,0 +1,55 @@
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    internal class AvaloniaPropertyBindingObservable<T> : LightweightObservableBase<BindingValue<T>>, IDescription
+    {
+        private readonly WeakReference<IAvaloniaObject> _target;
+        private readonly AvaloniaProperty _property;
+        private T _value;
+
+        public AvaloniaPropertyBindingObservable(
+            IAvaloniaObject target,
+            AvaloniaProperty property)
+        {
+            _target = new WeakReference<IAvaloniaObject>(target);
+            _property = property;
+        }
+
+        public string Description => $"{_target.GetType().Name}.{_property.Name}";
+
+        protected override void Initialize()
+        {
+            if (_target.TryGetTarget(out var target))
+            {
+                _value = (T)target.GetValue(_property);
+                target.PropertyChanged += PropertyChanged;
+            }
+        }
+
+        protected override void Deinitialize()
+        {
+            if (_target.TryGetTarget(out var target))
+            {
+                target.PropertyChanged -= PropertyChanged;
+            }
+        }
+
+        protected override void Subscribed(IObserver<BindingValue<T>> observer, bool first)
+        {
+            observer.OnNext(new BindingValue<T>(_value));
+        }
+
+        private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Property == _property)
+            {
+                _value = (T)e.NewValue;
+                PublishNext(new BindingValue<T>(_value));
+            }
+        }
+    }
+}

+ 61 - 0
src/Avalonia.Base/Reactive/BindingValueAdapter.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    internal class BindingValueAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        IObserver<T>
+    {
+        private readonly IObservable<T> _source;
+        private IDisposable? _subscription;
+
+        public BindingValueAdapter(IObservable<T> source) => _source = source;
+        public void OnCompleted() => PublishCompleted();
+        public void OnError(Exception error) => PublishError(error);
+        public void OnNext(T value) => PublishNext(BindingValue<T>.FromUntyped(value));
+        protected override void Subscribed() => _subscription = _source.Subscribe(this);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+    }
+
+    internal class BindingValueSubjectAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        ISubject<BindingValue<T>>
+    {
+        private readonly ISubject<T> _source;
+        private readonly Inner _inner;
+        private IDisposable? _subscription;
+
+        public BindingValueSubjectAdapter(ISubject<T> source)
+        {
+            _source = source;
+            _inner = new Inner(this);
+        }
+
+        public void OnCompleted() => _source.OnCompleted();
+        public void OnError(Exception error) => _source.OnError(error);
+        
+        public void OnNext(BindingValue<T> value)
+        {
+            if (value.HasValue)
+            {
+                _source.OnNext(value.Value);
+            }
+        }
+
+        protected override void Subscribed() => _subscription = _source.Subscribe(_inner);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+
+        private class Inner : IObserver<T>
+        {
+            private readonly BindingValueSubjectAdapter<T> _owner;
+
+            public Inner(BindingValueSubjectAdapter<T> owner) => _owner = owner;
+
+            public void OnCompleted() => _owner.PublishCompleted();
+            public void OnError(Exception error) => _owner.PublishError(error);
+            public void OnNext(T value) => _owner.PublishNext(BindingValue<T>.FromUntyped(value));
+        }
+    }
+}

+ 35 - 0
src/Avalonia.Base/Reactive/BindingValueExtensions.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    public static class BindingValueExtensions
+    {
+        public static IObservable<BindingValue<T>> ToBindingValue<T>(this IObservable<T> source)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            return new BindingValueAdapter<T>(source);
+        }
+
+        public static ISubject<BindingValue<T>> ToBindingValue<T>(this ISubject<T> source)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            return new BindingValueSubjectAdapter<T>(source);
+        }
+
+        public static IObservable<object> ToUntyped<T>(this IObservable<BindingValue<T>> source)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            return new UntypedBindingAdapter<T>(source);
+        }
+
+        public static ISubject<object> ToUntyped<T>(this ISubject<BindingValue<T>> source)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            return new UntypedBindingSubjectAdapter<T>(source);
+        }
+    }
+}

+ 63 - 0
src/Avalonia.Base/Reactive/TypedBindingAdapter.cs

@@ -0,0 +1,63 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Logging;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    internal class TypedBindingAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        IObserver<BindingValue<object>>
+    {
+        private readonly IAvaloniaObject _target;
+        private readonly AvaloniaProperty<T> _property;
+        private readonly IObservable<BindingValue<object>> _source;
+        private IDisposable? _subscription;
+
+        public TypedBindingAdapter(
+            IAvaloniaObject target,
+            AvaloniaProperty<T> property,
+            IObservable<BindingValue<object>> source)
+        {
+            _target = target;
+            _property = property;
+            _source = source;
+        }
+
+        public void OnNext(BindingValue<object> value)
+        {
+            try
+            {
+                PublishNext(value.Convert<T>());
+            }
+            catch (InvalidCastException e)
+            {
+                Logger.TryGet(LogEventLevel.Error)?.Log(
+                    LogArea.Binding,
+                    _target,
+                    "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
+                    _property.Name,
+                    _property.PropertyType,
+                    value.Value,
+                    value.Value?.GetType());
+                PublishNext(BindingValue<T>.BindingError(e));
+            }
+        }
+
+        public void OnCompleted() => PublishCompleted();
+        public void OnError(Exception error) => PublishError(error);
+
+        public static IObservable<BindingValue<T>> Create(
+            IAvaloniaObject target,
+            AvaloniaProperty<T> property,
+            IObservable<BindingValue<object>> source)
+        {
+            return source is IObservable<BindingValue<T>> result ?
+                result :
+                new TypedBindingAdapter<T>(target, property, source);
+        }
+
+        protected override void Subscribed() => _subscription = _source.Subscribe(this);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+    }
+}

+ 57 - 0
src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    internal class UntypedBindingAdapter<T> : SingleSubscriberObservableBase<object?>,
+        IObserver<BindingValue<T>>
+    {
+        private readonly IObservable<BindingValue<T>> _source;
+        private IDisposable? _subscription;
+
+        public UntypedBindingAdapter(IObservable<BindingValue<T>> source) => _source = source;
+        public void OnCompleted() => PublishCompleted();
+        public void OnError(Exception error) => PublishError(error);
+        public void OnNext(BindingValue<T> value) => value.ToUntyped();
+        protected override void Subscribed() => _subscription = _source.Subscribe(this);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+    }
+
+    internal class UntypedBindingSubjectAdapter<T> : SingleSubscriberObservableBase<object?>,
+        ISubject<object?>
+    {
+        private readonly ISubject<BindingValue<T>> _source;
+        private readonly Inner _inner;
+        private IDisposable? _subscription;
+
+        public UntypedBindingSubjectAdapter(ISubject<BindingValue<T>> source)
+        {
+            _source = source;
+            _inner = new Inner(this);
+        }
+
+        public void OnCompleted() => _source.OnCompleted();
+        public void OnError(Exception error) => _source.OnError(error);
+        public void OnNext(object? value)
+        {
+            _source.OnNext(BindingValue<T>.FromUntyped(value));
+        }
+
+        protected override void Subscribed() => _subscription = _source.Subscribe(_inner);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+
+        private class Inner : IObserver<BindingValue<T>>
+        {
+            private readonly UntypedBindingSubjectAdapter<T> _owner;
+
+            public Inner(UntypedBindingSubjectAdapter<T> owner) => _owner = owner;
+
+            public void OnCompleted() => _owner.PublishCompleted();
+            public void OnError(Exception error) => _owner.PublishError(error);
+            public void OnNext(BindingValue<T> value) => _owner.PublishNext(value.ToUntyped());
+        }
+    }
+}

+ 57 - 27
src/Avalonia.Base/StyledPropertyBase.cs

@@ -2,14 +2,17 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
+using Avalonia.Data;
+using Avalonia.Reactive;
 
 namespace Avalonia
 {
     /// <summary>
     /// Base class for styled properties.
     /// </summary>
-    public class StyledPropertyBase<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
+    public abstract class StyledPropertyBase<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
     {
         private bool _inherits;
 
@@ -124,48 +127,75 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Overrides the validation function for the specified type.
+        /// Gets the string representation of the property.
         /// </summary>
-        /// <typeparam name="THost">The type.</typeparam>
-        /// <param name="validate">The validation function.</param>
-        public void OverrideValidation<THost>(Func<THost, TValue, TValue> validate)
-            where THost : IAvaloniaObject
+        /// <returns>The property's string representation.</returns>
+        public override string ToString()
         {
-            Func<IAvaloniaObject, TValue, TValue> f;
+            return Name;
+        }
 
-            if (validate != null)
+        /// <inheritdoc/>
+        object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
+
+        /// <inheritdoc/>
+        internal override void NotifyInitialized(IAvaloniaObject o)
+        {
+            var e = new AvaloniaPropertyChangedEventArgs<TValue>(
+                o,
+                this,
+                default,
+                o.GetValue(this),
+                BindingPriority.Unset);
+            NotifyInitialized(e);
+        }
+
+        /// <inheritdoc/>
+        internal override object RouteGetValue(IAvaloniaObject o)
+        {
+            return o.GetValue<TValue>(this);
+        }
+
+        /// <inheritdoc/>
+        internal override void RouteSetValue(
+            IAvaloniaObject o,
+            object value,
+            BindingPriority priority)
+        {
+            var v = TryConvert(value);
+
+            if (v.HasValue)
             {
-                f = Cast(validate);
+                o.SetValue<TValue>(this, (TValue)v.Value, priority);
             }
-            else
+            else if (v.Type == BindingValueType.UnsetValue)
             {
-                // Passing null to the validation function means that the property metadata merge
-                // will take the base validation function, so instead use an empty validation.
-                f = (o, v) => v;
+                o.ClearValue(this);
+            }
+            else if (v.HasError)
+            {
+                throw v.Error;
             }
-
-            base.OverrideMetadata(typeof(THost), new StyledPropertyMetadata<TValue>(validate: f));
         }
 
-        /// <summary>
-        /// Gets the string representation of the property.
-        /// </summary>
-        /// <returns>The property's string representation.</returns>
-        public override string ToString()
+        /// <inheritdoc/>
+        internal override IDisposable RouteBind(
+            IAvaloniaObject o,
+            IObservable<BindingValue<object>> source,
+            BindingPriority priority)
         {
-            return Name;
+            var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
+            return o.Bind<TValue>(this, adapter, priority);
         }
 
         /// <inheritdoc/>
-        Func<IAvaloniaObject, object, object> IStyledPropertyAccessor.GetValidationFunc(Type type)
+        internal override void RouteInheritanceParentChanged(
+            AvaloniaObject o,
+            IAvaloniaObject oldParent)
         {
-            Contract.Requires<ArgumentNullException>(type != null);
-            return ((IStyledPropertyMetadata)base.GetMetadata(type)).Validate;
+            o.InheritanceParentChanged(this, oldParent);
         }
 
-        /// <inheritdoc/>
-        object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
-
         private object GetDefaultBoxedValue(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);

+ 0 - 15
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@@ -16,16 +16,13 @@ namespace Avalonia
         /// Initializes a new instance of the <see cref="StyledPropertyMetadata{TValue}"/> class.
         /// </summary>
         /// <param name="defaultValue">The default value of the property.</param>
-        /// <param name="validate">A validation function.</param>
         /// <param name="defaultBindingMode">The default binding mode.</param>
         public StyledPropertyMetadata(
             TValue defaultValue = default,
-            Func<IAvaloniaObject, TValue, TValue> validate = null,
             BindingMode defaultBindingMode = BindingMode.Default)
                 : base(defaultBindingMode)
         {
             DefaultValue = new BoxedValue<TValue>(defaultValue);
-            Validate = validate;
         }
 
         /// <summary>
@@ -33,15 +30,8 @@ namespace Avalonia
         /// </summary>
         internal BoxedValue<TValue> DefaultValue { get; private set; }
 
-        /// <summary>
-        /// Gets the validation callback.
-        /// </summary>
-        public Func<IAvaloniaObject, TValue, TValue> Validate { get; private set; }
-
         object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
 
-        Func<IAvaloniaObject, object, object> IStyledPropertyMetadata.Validate => Cast(Validate);
-
         /// <inheritdoc/>
         public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
         {
@@ -53,11 +43,6 @@ namespace Avalonia
                 {
                     DefaultValue = src.DefaultValue;
                 }
-
-                if (Validate == null)
-                {
-                    Validate = src.Validate;
-                }
             }
         }
 

+ 21 - 0
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@@ -129,6 +129,27 @@ namespace Avalonia.Utilities
             _entries[TryFindEntry(property.Id).Item1].Value = value;
         }
 
+        public void Remove(AvaloniaProperty property)
+        {
+            var (index, found) = TryFindEntry(property.Id);
+
+            if (found)
+            {
+                Entry[] entries = new Entry[_entries.Length - 1];
+                int ix = 0;
+
+                for (int i = 0; i < _entries.Length; ++i)
+                {
+                    if (i != index)
+                    {
+                        entries[ix++] = _entries[i];
+                    }
+                }
+
+                _entries = entries;
+            }
+        }
+
         public Dictionary<AvaloniaProperty, TValue> ToDictionary()
         {
             var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);

+ 11 - 0
src/Avalonia.Base/Utilities/TypeUtilities.cs

@@ -289,6 +289,17 @@ namespace Avalonia.Utilities
             return TryConvertImplicit(type, value, out object result) ? result : Default(type);
         }
 
+        public static T ConvertImplicit<T>(object value)
+        {
+            if (TryConvertImplicit(typeof(T), value, out var result))
+            {
+                return (T)result;
+            }
+
+            throw new InvalidCastException(
+                $"Unable to convert object '{value ?? "(null)"}' of type '{value?.GetType()}' to type '{typeof(T)}'.");
+        }
+
         /// <summary>
         /// Gets the default value for the specified type.
         /// </summary>

+ 154 - 128
src/Avalonia.Base/ValueStore.cs

@@ -1,205 +1,231 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Data;
+using Avalonia.PropertyStore;
 using Avalonia.Utilities;
 
+#nullable enable
+
 namespace Avalonia
 {
-    internal class ValueStore : IPriorityValueOwner
+    internal class ValueStore : IValueSink
     {
-        private readonly AvaloniaPropertyValueStore<object> _propertyValues;
-        private readonly AvaloniaPropertyValueStore<object> _deferredSetters;
         private readonly AvaloniaObject _owner;
+        private readonly IValueSink _sink;
+        private readonly AvaloniaPropertyValueStore<object> _values;
 
         public ValueStore(AvaloniaObject owner)
         {
-            _owner = owner;
-            _propertyValues = new AvaloniaPropertyValueStore<object>();
-            _deferredSetters = new AvaloniaPropertyValueStore<object>();
+            _sink = _owner = owner;
+            _values = new AvaloniaPropertyValueStore<object>();
         }
 
-        public IDisposable AddBinding(
-            AvaloniaProperty property,
-            IObservable<object> source,
-            BindingPriority priority)
+        public bool IsAnimating(AvaloniaProperty property)
         {
-            PriorityValue priorityValue;
-
-            if (_propertyValues.TryGetValue(property, out var v))
+            if (_values.TryGetValue(property, out var slot))
             {
-                priorityValue = v as PriorityValue;
-
-                if (priorityValue == null)
+                if (slot is IValue v)
                 {
-                    priorityValue = CreatePriorityValue(property);
-                    priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
-                    _propertyValues.SetValue(property, priorityValue);
+                    return v.ValuePriority < BindingPriority.LocalValue;
                 }
             }
-            else
-            {
-                priorityValue = CreatePriorityValue(property);
-                _propertyValues.AddValue(property, priorityValue);
-            }
 
-            return priorityValue.Add(source, (int)priority);
+            return false;
         }
 
-        public void AddValue(AvaloniaProperty property, object value, int priority)
+        public bool IsSet(AvaloniaProperty property)
         {
-            PriorityValue priorityValue;
+            return TryGetValueUntyped(property, out _);
+        }
 
-            if (_propertyValues.TryGetValue(property, out var v))
+        public bool TryGetValue<T>(StyledPropertyBase<T> property, out T value)
+        {
+            if (_values.TryGetValue(property, out var slot))
             {
-                priorityValue = v as PriorityValue;
-
-                if (priorityValue == null)
+                if (slot is IValue<T> v)
                 {
-                    if (priority == (int)BindingPriority.LocalValue)
-                    {
-                        Validate(property, ref value);
-                        _propertyValues.SetValue(property, value);
-                        Changed(property, priority, v, value);
-                        return;
-                    }
-                    else
+                    if (v.Value.HasValue)
                     {
-                        priorityValue = CreatePriorityValue(property);
-                        priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
-                        _propertyValues.SetValue(property, priorityValue);
+                        value = v.Value.Value;
+                        return true;
                     }
                 }
-            }
-            else
-            {
-                if (value == AvaloniaProperty.UnsetValue)
+                else
                 {
-                    return;
+                    value = (T)slot;
+                    return true;
                 }
+            }
+
+            value = default!;
+            return false;
+        }
 
-                if (priority == (int)BindingPriority.LocalValue)
+        public bool TryGetValueUntyped(AvaloniaProperty property, out object? value)
+        {
+            if (_values.TryGetValue(property, out var slot))
+            {
+                if (slot is IValue v)
                 {
-                    Validate(property, ref value);
-                    _propertyValues.AddValue(property, value);
-                    Changed(property, priority, AvaloniaProperty.UnsetValue, value);
-                    return;
+                    if (v.Value.HasValue)
+                    {
+                        value = v.Value.Value;
+                        return true;
+                    }
                 }
                 else
                 {
-                    priorityValue = CreatePriorityValue(property);
-                    _propertyValues.AddValue(property, priorityValue);
+                    value = slot;
+                    return true;
                 }
             }
 
-            priorityValue.SetValue(value, priority);
-        }
-
-        public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
-        {
-            _owner.BindingNotificationReceived(property, notification);
-        }
-
-        public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
-        {
-            _owner.PriorityValueChanged(property, priority, oldValue, newValue);
+            value = default;
+            return false;
         }
 
-        public IDictionary<AvaloniaProperty, object> GetSetValues()
+        public void SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
         {
-            return _propertyValues.ToDictionary();
+            if (_values.TryGetValue(property, out var slot))
+            {
+                SetExisting(slot, property, value, priority);
+            }
+            else if (priority == BindingPriority.LocalValue)
+            {
+                _values.AddValue(property, (object)value!);
+                _sink.ValueChanged(property, priority, default, value);
+            }
+            else
+            {
+                var entry = new ConstantValueEntry<T>(property, value, priority);
+                _values.AddValue(property, entry);
+                _sink.ValueChanged(property, priority, default, value);
+            }
         }
 
-        public void LogError(AvaloniaProperty property, Exception e)
+        public IDisposable AddBinding<T>(
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority)
         {
-            _owner.LogBindingError(property, e);
+            if (_values.TryGetValue(property, out var slot))
+            {
+                return BindExisting(slot, property, source, priority);
+            }
+            else
+            {
+                var entry = new BindingEntry<T>(_owner, property, source, priority, this);
+                _values.AddValue(property, entry);
+                entry.Start();
+                return entry;
+            }
         }
 
-        public object GetValue(AvaloniaProperty property)
+        public void ClearLocalValue<T>(StyledPropertyBase<T> property)
         {
-            var result = AvaloniaProperty.UnsetValue;
-
-            if (_propertyValues.TryGetValue(property, out var value))
+            if (_values.TryGetValue(property, out var slot))
             {
-                result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
-            }
+                if (slot is PriorityValue<T> p)
+                {
+                    p.ClearLocalValue();
+                }
+                else
+                {
+                    var remove = slot is ConstantValueEntry<T> c ?
+                        c.Priority == BindingPriority.LocalValue : 
+                        !(slot is IPriorityValueEntry<T>);
 
-            return result;
+                    if (remove)
+                    {
+                        var old = TryGetValue(property, out var value) ? value : default;
+                        _values.Remove(property);
+                        _sink.ValueChanged(
+                            property,
+                            BindingPriority.LocalValue,
+                            old,
+                            BindingValue<T>.Unset);
+                    }
+                }
+            }
         }
 
-        public bool IsAnimating(AvaloniaProperty property)
+        void IValueSink.ValueChanged<T>(
+            StyledPropertyBase<T> property,
+            BindingPriority priority,
+            Optional<T> oldValue,
+            BindingValue<T> newValue)
         {
-            return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
+            _sink.ValueChanged(property, priority, oldValue, newValue);
         }
 
-        public bool IsSet(AvaloniaProperty property)
+        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry)
         {
-            if (_propertyValues.TryGetValue(property, out var value))
+            if (_values.TryGetValue(property, out var slot))
             {
-                return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
+                if (slot == entry)
+                {
+                    _values.Remove(property);
+                }
             }
-
-            return false;
         }
 
-        public void Revalidate(AvaloniaProperty property)
+        private void SetExisting<T>(
+            object slot,
+            StyledPropertyBase<T> property,
+            T value,
+            BindingPriority priority)
         {
-            if (_propertyValues.TryGetValue(property, out var value))
+            if (slot is IPriorityValueEntry<T> e)
             {
-                (value as PriorityValue)?.Revalidate();
+                var priorityValue = new PriorityValue<T>(_owner, property, this, e);
+                _values.SetValue(property, priorityValue);
+                priorityValue.SetValue(value, priority);
             }
-        }
-
-        public void VerifyAccess() => _owner.VerifyAccess();
-
-        private PriorityValue CreatePriorityValue(AvaloniaProperty property)
-        {
-            var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
-            Func<object, object> validate2 = null;
-
-            if (validate != null)
+            else if (slot is PriorityValue<T> p)
             {
-                validate2 = v => validate(_owner, v);
+                p.SetValue(value, priority);
+            }
+            else if (priority == BindingPriority.LocalValue)
+            {
+                var old = (T)slot;
+                _values.SetValue(property, (object)value!);
+                _sink.ValueChanged(property, priority, old, value);
+            }
+            else
+            {
+                var existing = new ConstantValueEntry<T>(property, (T)slot, BindingPriority.LocalValue);
+                var priorityValue = new PriorityValue<T>(_owner, property, this, existing);
+                priorityValue.SetValue(value, priority);
+                _values.SetValue(property, priorityValue);
             }
-
-            return new PriorityValue(
-                this,
-                property,
-                property.PropertyType,
-                validate2);
         }
 
-        private void Validate(AvaloniaProperty property, ref object value)
+        private IDisposable BindExisting<T>(
+            object slot,
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority)
         {
-            var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
+            PriorityValue<T> priorityValue;
 
-            if (validate != null && value != AvaloniaProperty.UnsetValue)
+            if (slot is IPriorityValueEntry<T> e)
             {
-                value = validate(_owner, value);
+                priorityValue = new PriorityValue<T>(_owner, property, this, e);
             }
-        }
-
-        private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)
-        {
-            if (_deferredSetters.TryGetValue(property, out var deferredSetter))
+            else if (slot is PriorityValue<T> p)
             {
-                return (DeferredSetter<T>)deferredSetter;
+                priorityValue = p;
+            }
+            else
+            {
+                var existing = new ConstantValueEntry<T>(property, (T)slot, BindingPriority.LocalValue);
+                priorityValue = new PriorityValue<T>(_owner, property, this, existing);
             }
 
-            var newDeferredSetter = new DeferredSetter<T>();
-
-            _deferredSetters.AddValue(property, newDeferredSetter);
-
-            return newDeferredSetter;
-        }
-
-        public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property)
-        {
-            return GetDeferredSetter<object>(property);
-        }
-
-        public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property)
-        {
-            return GetDeferredSetter<T>(property);
+            var binding = priorityValue.AddBinding(source, priority);
+            _values.SetValue(property, priorityValue);
+            binding.Start();
+            return binding;
         }
     }
 }

+ 12 - 12
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -201,8 +201,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<double> ColumnHeaderHeightProperty =
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(ColumnHeaderHeight),
-                defaultValue: double.NaN,
-                validate: ValidateColumnHeaderHeight);
+                defaultValue: double.NaN/*,
+                validate: ValidateColumnHeaderHeight*/);
 
         private static double ValidateColumnHeaderHeight(DataGrid grid, double value)
         {
@@ -261,8 +261,8 @@ namespace Avalonia.Controls
 
         public static readonly StyledProperty<int> FrozenColumnCountProperty =
             AvaloniaProperty.Register<DataGrid, int>(
-                nameof(FrozenColumnCount),
-                validate: ValidateFrozenColumnCount);
+                nameof(FrozenColumnCount)/*,
+                validate: ValidateFrozenColumnCount*/);
 
         /// <summary>
         /// Gets or sets the number of columns that the user cannot scroll horizontally.
@@ -395,8 +395,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<double> MaxColumnWidthProperty =
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(MaxColumnWidth),
-                defaultValue: DATAGRID_defaultMaxColumnWidth,
-                validate: ValidateMaxColumnWidth);
+                defaultValue: DATAGRID_defaultMaxColumnWidth/*,
+                validate: ValidateMaxColumnWidth*/);
 
         private static double ValidateMaxColumnWidth(DataGrid grid, double value)
         {
@@ -433,8 +433,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<double> MinColumnWidthProperty =
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(MinColumnWidth),
-                defaultValue: DATAGRID_defaultMinColumnWidth,
-                validate: ValidateMinColumnWidth);
+                defaultValue: DATAGRID_defaultMinColumnWidth/*,
+                validate: ValidateMinColumnWidth*/);
 
         private static double ValidateMinColumnWidth(DataGrid grid, double value)
         {
@@ -482,8 +482,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<double> RowHeightProperty =
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(RowHeight),
-                defaultValue: double.NaN,
-                validate: ValidateRowHeight);
+                defaultValue: double.NaN/*,
+                validate: ValidateRowHeight*/);
         private static double ValidateRowHeight(DataGrid grid, double value)
         {
             if (value < DataGridRow.DATAGRIDROW_minimumHeight)
@@ -510,8 +510,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<double> RowHeaderWidthProperty =
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(RowHeaderWidth),
-                defaultValue: double.NaN,
-                validate: ValidateRowHeaderWidth);
+                defaultValue: double.NaN/*,
+                validate: ValidateRowHeaderWidth*/);
         private static double ValidateRowHeaderWidth(DataGrid grid, double value)
         {
             if (value < DATAGRID_minimumRowHeaderWidth)

+ 2 - 2
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@@ -67,8 +67,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<double> SublevelIndentProperty =
             AvaloniaProperty.Register<DataGridRowGroupHeader, double>(
                 nameof(SublevelIndent),
-                defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent,
-                validate: ValidateSublevelIndent);
+                defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent/*,
+                validate: ValidateSublevelIndent*/);
 
         private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value)
         {

+ 8 - 8
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -377,8 +377,8 @@ namespace Avalonia.Controls
         /// dependency property.</value>
         public static readonly StyledProperty<int> MinimumPrefixLengthProperty =
             AvaloniaProperty.Register<AutoCompleteBox, int>(
-                nameof(MinimumPrefixLength), 1,
-                validate: ValidateMinimumPrefixLength);
+                nameof(MinimumPrefixLength), 1/*,
+                validate: ValidateMinimumPrefixLength*/);
 
         /// <summary>
         /// Identifies the
@@ -391,8 +391,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<TimeSpan> MinimumPopulateDelayProperty =
             AvaloniaProperty.Register<AutoCompleteBox, TimeSpan>(
                 nameof(MinimumPopulateDelay),
-                TimeSpan.Zero,
-                validate: ValidateMinimumPopulateDelay);
+                TimeSpan.Zero/*,
+                validate: ValidateMinimumPopulateDelay*/);
 
         /// <summary>
         /// Identifies the
@@ -405,8 +405,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<double> MaxDropDownHeightProperty =
             AvaloniaProperty.Register<AutoCompleteBox, double>(
                 nameof(MaxDropDownHeight),
-                double.PositiveInfinity,
-                validate: ValidateMaxDropDownHeight);
+                double.PositiveInfinity/*,
+                validate: ValidateMaxDropDownHeight*/);
 
         /// <summary>
         /// Identifies the
@@ -494,8 +494,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<AutoCompleteFilterMode> FilterModeProperty =
             AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteFilterMode>(
                 nameof(FilterMode),
-                defaultValue: AutoCompleteFilterMode.StartsWith,
-                validate: ValidateFilterMode);
+                defaultValue: AutoCompleteFilterMode.StartsWith/*,
+                validate: ValidateFilterMode*/);
 
         /// <summary>
         /// Identifies the

+ 9 - 9
src/Avalonia.Controls/Button.cs

@@ -306,18 +306,13 @@ namespace Avalonia.Controls
                 }
             }
         }
-
-        protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
-        {
-            IsPressed = false;
-        }
-
-        protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
+        
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
-            base.UpdateDataValidation(property, status);
+            base.UpdateDataValidation(property, value);
             if (property == CommandProperty)
             {
-                if (status?.ErrorType == BindingErrorType.Error)
+                if (value.Type == BindingValueType.BindingError)
                 {
                     if (_commandCanExecute)
                     {
@@ -328,6 +323,11 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+        {
+            IsPressed = false;
+        }
+
         /// <summary>
         /// Called when the <see cref="Command"/> property changes.
         /// </summary>

+ 2 - 2
src/Avalonia.Controls/Calendar/Calendar.cs

@@ -351,8 +351,8 @@ namespace Avalonia.Controls
 
         public static readonly StyledProperty<CalendarMode> DisplayModeProperty =
             AvaloniaProperty.Register<Calendar, CalendarMode>(
-                nameof(DisplayMode),
-                validate: ValidateDisplayMode);
+                nameof(DisplayMode)/*,
+                validate: ValidateDisplayMode*/);
         /// <summary>
         /// Gets or sets a value indicating whether the calendar is displayed in
         /// months, years, or decades.

+ 12 - 6
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -189,14 +189,14 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<DatePickerFormat> SelectedDateFormatProperty =
             AvaloniaProperty.Register<DatePicker, DatePickerFormat>(
                 nameof(SelectedDateFormat),
-                defaultValue: DatePickerFormat.Short,
-                validate: ValidateSelectedDateFormat);
+                defaultValue: DatePickerFormat.Short/*,
+                validate: ValidateSelectedDateFormat*/);
 
         public static readonly StyledProperty<string> CustomDateFormatStringProperty =
             AvaloniaProperty.Register<DatePicker, string>(
                 nameof(CustomDateFormatString),
-                defaultValue: "d",
-                validate: ValidateDateFormatString);
+                defaultValue: "d"/*,
+                validate: ValidateDateFormatString*/);
 
         public static readonly DirectProperty<DatePicker, string> TextProperty =
             AvaloniaProperty.RegisterDirect<DatePicker, string>(
@@ -512,11 +512,17 @@ namespace Avalonia.Controls
             base.OnTemplateApplied(e);
         }
 
-        protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
         {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
             if (property == SelectedDateProperty)
             {
-                DataValidationErrors.SetError(this, status.Error);
+                DataValidationErrors.SetError(this, newValue.Error);
             }
         }
 

+ 5 - 5
src/Avalonia.Controls/MenuItem.cs

@@ -26,8 +26,8 @@ namespace Avalonia.Controls
         /// </summary>
         public static readonly DirectProperty<MenuItem, ICommand> CommandProperty =
             Button.CommandProperty.AddOwner<MenuItem>(
-                menuItem => menuItem.Command, 
-                (menuItem, command) => menuItem.Command = command, 
+                menuItem => menuItem.Command,
+                (menuItem, command) => menuItem.Command = command,
                 enableDataValidation: true);
 
         /// <summary>
@@ -394,12 +394,12 @@ namespace Avalonia.Controls
             }
         }
 
-        protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
-            base.UpdateDataValidation(property, status);
+            base.UpdateDataValidation(property, value);
             if (property == CommandProperty)
             {
-                if (status?.ErrorType == BindingErrorType.Error)
+                if (value.Type == BindingValueType.BindingError)
                 {
                     if (_commandCanExecute)
                     {

+ 3 - 3
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -58,7 +58,7 @@ namespace Avalonia.Controls
         /// Defines the <see cref="Increment"/> property.
         /// </summary>
         public static readonly StyledProperty<double> IncrementProperty =
-            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement);
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d/*, validate: OnCoerceIncrement*/);
 
         /// <summary>
         /// Defines the <see cref="IsReadOnly"/> property.
@@ -70,13 +70,13 @@ namespace Avalonia.Controls
         /// Defines the <see cref="Maximum"/> property.
         /// </summary>
         public static readonly StyledProperty<double> MaximumProperty =
-            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum);
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue/*, validate: OnCoerceMaximum*/);
 
         /// <summary>
         /// Defines the <see cref="Minimum"/> property.
         /// </summary>
         public static readonly StyledProperty<double> MinimumProperty =
-            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum);
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue/*, validate: OnCoerceMinimum*/);
 
         /// <summary>
         /// Defines the <see cref="ParsingNumberStyle"/> property.

+ 1 - 1
src/Avalonia.Controls/Primitives/ScrollBar.cs

@@ -73,7 +73,7 @@ namespace Avalonia.Controls.Primitives
                 this.GetObservable(ViewportSizeProperty).Select(_ => Unit.Default),
                 this.GetObservable(VisibilityProperty).Select(_ => Unit.Default))
                 .Select(_ => CalculateIsVisible());
-            Bind(IsVisibleProperty, isVisible, BindingPriority.Style);
+            this.Bind(IsVisibleProperty, isVisible, BindingPriority.Style);
         }
 
         /// <summary>

+ 12 - 15
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -7,6 +7,7 @@ using System;
 using System.Collections;
 using System.Collections.Specialized;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Layout;
 
@@ -374,41 +375,37 @@ namespace Avalonia.Controls
             _viewportManager.ResetScrollers();
         }
 
-        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+        protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
         {
-            var property = args.Property;
-
             if (property == ItemsProperty)
             {
-                var newValue = (IEnumerable)args.NewValue;
-                var newDataSource = newValue as ItemsSourceView;
-                if (newValue != null && newDataSource == null)
+                var newEnumerable = newValue.ValueOrDefault<IEnumerable>();
+                var newDataSource = newEnumerable as ItemsSourceView;
+                if (newEnumerable != null && newDataSource == null)
                 {
-                    newDataSource = new ItemsSourceView(newValue);
+                    newDataSource = new ItemsSourceView(newEnumerable);
                 }
 
                 OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
             }
             else if (property == ItemTemplateProperty)
             {
-                OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue);
+                OnItemTemplateChanged(oldValue.ValueOrDefault<IDataTemplate>(), newValue.ValueOrDefault<IDataTemplate>());
             }
             else if (property == LayoutProperty)
             {
-                OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue);
+                OnLayoutChanged(oldValue.ValueOrDefault<AttachedLayout>(), newValue.ValueOrDefault<AttachedLayout>());
             }
             else if (property == HorizontalCacheLengthProperty)
             {
-                _viewportManager.HorizontalCacheLength = (double)args.NewValue;
+                _viewportManager.HorizontalCacheLength = newValue.ValueOrDefault<double>();
             }
             else if (property == VerticalCacheLengthProperty)
             {
-                _viewportManager.VerticalCacheLength = (double)args.NewValue;
-            }
-            else
-            {
-                base.OnPropertyChanged(args);
+                _viewportManager.VerticalCacheLength = newValue.ValueOrDefault<double>();
             }
+
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
         }
 
         internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)

+ 0 - 2
src/Avalonia.Controls/ScrollViewer.cs

@@ -161,8 +161,6 @@ namespace Avalonia.Controls
         /// </summary>
         static ScrollViewer()
         {
-            AffectsValidation(ExtentProperty, OffsetProperty);
-            AffectsValidation(ViewportProperty, OffsetProperty);
             HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer>((x, e) => x.ScrollBarVisibilityChanged(e));
             VerticalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer>((x, e) => x.ScrollBarVisibilityChanged(e));
         }

+ 4 - 4
src/Avalonia.Controls/TextBox.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0);
 
         public static readonly DirectProperty<TextBox, string> TextProperty =
-            TextBlock.TextProperty.AddOwner<TextBox>(
+            TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>(
                 o => o.Text,
                 (o, v) => o.Text = v,
                 defaultBindingMode: BindingMode.TwoWay,
@@ -133,7 +133,7 @@ namespace Avalonia.Controls
                         return ScrollBarVisibility.Hidden;
                     }
                 });
-            Bind(
+            this.Bind(
                 ScrollViewer.HorizontalScrollBarVisibilityProperty,
                 horizontalScrollBarVisibility,
                 BindingPriority.Style);
@@ -700,11 +700,11 @@ namespace Avalonia.Controls
             }
         }
 
-        protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
             if (property == TextProperty)
             {
-                DataValidationErrors.SetError(this, status.Error);
+                DataValidationErrors.SetError(this, value.Error);
             }
         }
 

+ 4 - 3
src/Avalonia.Layout/StackLayout.cs

@@ -5,6 +5,7 @@
 
 using System;
 using System.Collections.Specialized;
+using Avalonia.Data;
 
 namespace Avalonia.Layout
 {
@@ -293,11 +294,11 @@ namespace Avalonia.Layout
             InvalidateLayout();
         }
 
-        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
         {
-            if (e.Property == OrientationProperty)
+            if (property == OrientationProperty)
             {
-                var orientation = (Orientation)e.NewValue;
+                var orientation = newValue.ValueOrDefault<Orientation>();
 
                 //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
                 //Horizontal Orientation means we have a Horizontal ScrollOrientation.

+ 18 - 15
src/Avalonia.Layout/UniformGridLayout.cs

@@ -5,6 +5,7 @@
 
 using System;
 using System.Collections.Specialized;
+using Avalonia.Data;
 
 namespace Avalonia.Layout
 {
@@ -436,40 +437,42 @@ namespace Avalonia.Layout
             gridState.ClearElementOnDataSourceChange(context, args);
         }
 
-        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+        protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
         {
-            if (args.Property == OrientationProperty)
+            if (property == OrientationProperty)
             {
-                var orientation = (Orientation)args.NewValue;
+                var orientation = newValue.ValueOrDefault<Orientation>();
 
                 //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation.
                 //i.e. the properties are the inverse of each other.
                 var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal;
                 _orientation.ScrollOrientation = scrollOrientation;
             }
-            else if (args.Property == MinColumnSpacingProperty)
+            else if (property == MinColumnSpacingProperty)
             {
-                _minColumnSpacing = (double)args.NewValue;
+                _minColumnSpacing = newValue.ValueOrDefault<double>();
             }
-            else if (args.Property == MinRowSpacingProperty)
+            else if (property == MinRowSpacingProperty)
             {
-                _minRowSpacing = (double)args.NewValue;
+                _minRowSpacing = newValue.ValueOrDefault<double>();
             }
-            else if (args.Property == ItemsJustificationProperty)
+            else if (property == ItemsJustificationProperty)
             {
-                _itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue;
+                _itemsJustification = newValue.ValueOrDefault<UniformGridLayoutItemsJustification>();
+                ;
             }
-            else if (args.Property == ItemsStretchProperty)
+            else if (property == ItemsStretchProperty)
             {
-                _itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue;
+                _itemsStretch = newValue.ValueOrDefault<UniformGridLayoutItemsStretch>();
+                ;
             }
-            else if (args.Property == MinItemWidthProperty)
+            else if (property == MinItemWidthProperty)
             {
-                _minItemWidth = (double)args.NewValue;
+                _minItemWidth = newValue.ValueOrDefault<double>();
             }
-            else if (args.Property == MinItemHeightProperty)
+            else if (property == MinItemHeightProperty)
             {
-                _minItemHeight = (double)args.NewValue;
+                _minItemHeight = newValue.ValueOrDefault<double>();
             }
 
             InvalidateLayout();

+ 5 - 1
src/Avalonia.Styling/StyledElement.cs

@@ -478,7 +478,11 @@ namespace Avalonia
                     OnAttachedToLogicalTreeCore(e);
                 }
 
-                RaisePropertyChanged(ParentProperty, old, Parent, BindingPriority.LocalValue);
+                RaisePropertyChanged(
+                    ParentProperty,
+                    new Optional<IStyledElement>(old),
+                    new BindingValue<IStyledElement>(Parent),
+                    BindingPriority.LocalValue);
             }
         }
 

+ 5 - 1
src/Avalonia.Visuals/Visual.cs

@@ -433,7 +433,11 @@ namespace Avalonia
         /// <param name="newParent">The new visual parent.</param>
         protected virtual void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
         {
-            RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue);
+            RaisePropertyChanged(
+                VisualParentProperty,
+                new Optional<IVisual>(oldParent),
+                new BindingValue<IVisual>(newParent),
+                BindingPriority.LocalValue);
         }
 
         protected override sealed void LogBindingError(AvaloniaProperty property, Exception e)

+ 1 - 21
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs

@@ -1,7 +1,6 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests
@@ -16,31 +15,12 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("foodefault", target.GetValue(Class2.FooProperty));
         }
 
-        [Fact]
-        public void AddOwnered_Property_Does_Not_Retain_Validation()
-        {
-            var target = new Class2();
-
-            target.SetValue(Class2.FooProperty, "throw");
-        }
-
         private class Class1 : AvaloniaObject
         {
             public static readonly StyledProperty<string> FooProperty =
                 AvaloniaProperty.Register<Class1, string>(
                     "Foo",
-                    "foodefault",
-                    validate: ValidateFoo);
-
-            private static string ValidateFoo(AvaloniaObject arg1, string arg2)
-            {
-                if (arg2 == "throw")
-                {
-                    throw new IndexOutOfRangeException();
-                }
-
-                return arg2;
-            }
+                    "foodefault");
         }
 
         private class Class2 : AvaloniaObject

+ 0 - 8
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs

@@ -16,14 +16,6 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("foodefault", target.GetValue(Class2.FooProperty));
         }
 
-        [Fact]
-        public void AddOwnered_Property_Retains_Validation()
-        {
-            var target = new Class2();
-
-            Assert.Throws<IndexOutOfRangeException>(() => target.SetValue(Class2.FooProperty, "throw"));
-        }
-
         [Fact]
         public void AvaloniaProperty_Initialized_Is_Called_For_Attached_Property()
         {

+ 220 - 35
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -3,18 +3,15 @@
 
 using System;
 using System.ComponentModel;
-using System.Reactive.Concurrency;
 using System.Reactive.Linq;
 using System.Reactive.Subjects;
 using System.Threading;
 using System.Threading.Tasks;
 using Avalonia.Data;
 using Avalonia.Logging;
-using Avalonia.Markup.Data;
 using Avalonia.Platform;
 using Avalonia.Threading;
 using Avalonia.UnitTests;
-using Avalonia.Diagnostics;
 using Microsoft.Reactive.Testing;
 using Moq;
 using Xunit;
@@ -26,13 +23,158 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Bind_Sets_Current_Value()
         {
-            Class1 target = new Class1();
-            Class1 source = new Class1();
+            var target = new Class1();
+            var source = new Class1();
+            var property = Class1.FooProperty;
 
-            source.SetValue(Class1.FooProperty, "initial");
-            target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty));
+            source.SetValue(property, "initial");
+            target.Bind(property, source.GetObservable(property));
 
-            Assert.Equal("initial", target.GetValue(Class1.FooProperty));
+            Assert.Equal("initial", target.GetValue(property));
+        }
+
+        [Fact]
+        public void Bind_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
+            bool raised = false;
+
+            target.PropertyChanged += (s, e) =>
+                raised = e.Property == Class1.FooProperty &&
+                         (string)e.OldValue == "foodefault" &&
+                         (string)e.NewValue == "newvalue" &&
+                         e.Priority == BindingPriority.LocalValue;
+
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("newvalue");
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void PropertyChanged_Not_Raised_When_Value_Unchanged()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
+            var raised = 0;
+
+            target.PropertyChanged += (s, e) => ++raised;
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("newvalue");
+            source.OnNext("newvalue");
+
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Setting_LocalValue_Overrides_Binding_Until_Binding_Produces_Next_Value()
+        {
+            var target = new Class1();
+            var source = new Subject<string>();
+            var property = Class1.FooProperty;
+
+            target.Bind(property, source);
+            source.OnNext("foo");
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.SetValue(property, "bar");
+            Assert.Equal("bar", target.GetValue(property));
+
+            source.OnNext("baz"); 
+            Assert.Equal("baz", target.GetValue(property));
+        }
+
+        [Fact]
+        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 property = Class1.FooProperty;
+
+            target.Bind(property, source);
+            source.OnNext("foo");
+            target.SetValue(property, "bar");
+            source.OnNext("baz");
+            source.OnCompleted();
+
+            Assert.Equal("foodefault", target.GetValue(property));
+        }
+
+        [Fact]
+        public void Setting_Style_Value_Overrides_Binding_Permanently()
+        {
+            var target = new Class1();
+            var source = new Subject<string>();
+
+            target.Bind(Class1.FooProperty, source, BindingPriority.Style);
+            source.OnNext("foo");
+            Assert.Equal("foo", target.GetValue(Class1.FooProperty));
+
+            target.SetValue(Class1.FooProperty, "bar", BindingPriority.Style);
+            Assert.Equal("bar", target.GetValue(Class1.FooProperty));
+
+            source.OnNext("baz");
+            Assert.Equal("bar", target.GetValue(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void Second_LocalValue_Binding_Overrides_First()
+        {
+            var property = Class1.FooProperty;
+            var target = new Class1();
+            var source1 = new Subject<string>();
+            var source2 = new Subject<string>();
+
+            target.Bind(property, source1, BindingPriority.LocalValue);
+            target.Bind(property, source2, BindingPriority.LocalValue);
+
+            source1.OnNext("foo");
+            Assert.Equal("foo", target.GetValue(property));
+
+            source2.OnNext("bar");
+            Assert.Equal("bar", target.GetValue(property));
+
+            source1.OnNext("baz");
+            Assert.Equal("bar", target.GetValue(property));
+        }
+
+        [Fact]
+        public void Completing_Second_LocalValue_Binding_Reverts_To_First()
+        {
+            var property = Class1.FooProperty;
+            var target = new Class1();
+            var source1 = new Subject<string>();
+            var source2 = new Subject<string>();
+
+            target.Bind(property, source1, BindingPriority.LocalValue);
+            target.Bind(property, source2, BindingPriority.LocalValue);
+
+            source1.OnNext("foo");
+            source2.OnNext("bar");
+            source1.OnNext("baz");
+            source2.OnCompleted();
+
+            Assert.Equal("baz", target.GetValue(property));
+        }
+
+        [Fact]
+        public void Completing_StyleTrigger_Binding_Reverts_To_StyleBinding()
+        {
+            var property = Class1.FooProperty;
+            var target = new Class1();
+            var source1 = new Subject<string>();
+            var source2 = new Subject<string>();
+
+            target.Bind(property, source1, BindingPriority.Style);
+            target.Bind(property, source2, BindingPriority.StyleTrigger);
+
+            source1.OnNext("foo");
+            source2.OnNext("bar");
+            source2.OnCompleted();
+            source1.OnNext("baz");
+
+            Assert.Equal("baz", target.GetValue(property));
         }
 
         [Fact]
@@ -126,7 +268,7 @@ namespace Avalonia.Base.UnitTests
         public void Observable_Is_Unsubscribed_When_Subscription_Disposed()
         {
             var scheduler = new TestScheduler();
-            var source = scheduler.CreateColdObservable<object>();
+            var source = scheduler.CreateColdObservable<string>();
             var target = new Class1();
 
             var subscription = target.Bind(Class1.FooProperty, source);
@@ -191,13 +333,13 @@ namespace Avalonia.Base.UnitTests
 
             obj2.SetValue(Class1.FooProperty, "second", BindingPriority.Style);
 
-            Assert.Equal("second", obj1.GetValue(Class1.FooProperty));
+            Assert.Equal("first", obj1.GetValue(Class1.FooProperty));
             Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
 
             obj1.SetValue(Class1.FooProperty, "third", BindingPriority.Style);
 
             Assert.Equal("third", obj1.GetValue(Class1.FooProperty));
-            Assert.Equal("third", obj2.GetValue(Class1.FooProperty));
+            Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
         }
 
         [Fact]
@@ -302,41 +444,62 @@ namespace Avalonia.Base.UnitTests
         }
 
         [Fact]
-        public void BindingError_Does_Not_Cause_Target_Update()
+        public void Binding_Error_Reverts_To_Default_Value()
         {
             var target = new Class1();
-            var source = new Subject<object>();
+            var source = new Subject<BindingValue<string>>();
 
-            target.Bind(Class1.QuxProperty, source);
-            source.OnNext(6.7);
-            source.OnNext(new BindingNotification(
-                new InvalidOperationException("Foo"),
-                BindingErrorType.Error));
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("initial");
+            source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo")));
 
-            Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
+            Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
         }
 
         [Fact]
-        public void BindingNotification_With_FallbackValue_Causes_Target_Update()
+        public void Binding_Error_With_FallbackValue_Causes_Target_Update()
         {
             var target = new Class1();
-            var source = new Subject<object>();
+            var source = new Subject<BindingValue<string>>();
 
-            target.Bind(Class1.QuxProperty, source);
-            source.OnNext(6.7);
-            source.OnNext(new BindingNotification(
-                new InvalidOperationException("Foo"),
-                BindingErrorType.Error,
-                8.9));
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("initial");
+            source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo"), "bar"));
+
+            Assert.Equal("bar", target.GetValue(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void DataValidationError_Does_Not_Cause_Target_Update()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
+
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("initial");
+            source.OnNext(BindingValue<string>.DataValidationError(new InvalidOperationException("Foo")));
+
+            Assert.Equal("initial", target.GetValue(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void DataValidationError_With_FallbackValue_Causes_Target_Update()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
 
-            Assert.Equal(8.9, target.GetValue(Class1.QuxProperty));
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("initial");
+            source.OnNext(BindingValue<string>.DataValidationError(new InvalidOperationException("Foo"), "bar"));
+
+            Assert.Equal("bar", target.GetValue(Class1.FooProperty));
         }
 
         [Fact]
         public void Bind_Logs_Binding_Error()
         {
             var target = new Class1();
-            var source = new Subject<object>();
+            var source = new Subject<BindingValue<double>>();
             var called = false;
             var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}";
 
@@ -354,9 +517,7 @@ namespace Avalonia.Base.UnitTests
             {
                 target.Bind(Class1.QuxProperty, source);
                 source.OnNext(6.7);
-                source.OnNext(new BindingNotification(
-                    new InvalidOperationException("Foo"),
-                    BindingErrorType.Error));
+                source.OnNext(BindingValue<double>.BindingError(new InvalidOperationException("Foo")));
 
                 Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
                 Assert.True(called);
@@ -367,7 +528,7 @@ namespace Avalonia.Base.UnitTests
         public async Task Bind_With_Scheduler_Executes_On_Scheduler()
         {
             var target = new Class1();
-            var source = new Subject<object>();
+            var source = new Subject<double>();
             var currentThreadId = Thread.CurrentThread.ManagedThreadId;
 
             var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
@@ -426,13 +587,13 @@ namespace Avalonia.Base.UnitTests
         }
 
         [Fact]
-        public void IsAnimating_On_Property_With_Animation_Value_Returns_False()
+        public void IsAnimating_On_Property_With_Animation_Value_Returns_True()
         {
             var target = new Class1();
 
             target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation);
 
-            Assert.False(target.IsAnimating(Class1.FooProperty));
+            Assert.True(target.IsAnimating(Class1.FooProperty));
         }
 
         [Fact]
@@ -457,6 +618,30 @@ namespace Avalonia.Base.UnitTests
             Assert.True(target.IsAnimating(Class1.FooProperty));
         }
 
+        [Fact]
+        public void IsAnimating_On_Property_With_Local_Value_And_Animation_Binding_Returns_True()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<string>("foo");
+
+            target.SetValue(Class1.FooProperty, "bar");
+            target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
+
+            Assert.True(target.IsAnimating(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void IsAnimating_Returns_True_When_Animated_Value_Is_Same_As_Local_Value()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<string>("foo");
+
+            target.SetValue(Class1.FooProperty, "foo");
+            target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
+
+            Assert.True(target.IsAnimating(Class1.FooProperty));
+        }
+
         [Fact]
         public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation()
         {

+ 36 - 65
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs

@@ -11,59 +11,31 @@ namespace Avalonia.Base.UnitTests
     public class AvaloniaObjectTests_DataValidation
     {
         [Fact]
-        public void Setting_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
+        public void Binding_Non_Validated_Styled_Property_Does_Not_Call_UpdateDataValidation()
         {
             var target = new Class1();
+            var source = new Subject<BindingValue<int>>();
 
-            target.SetValue(Class1.NonValidatedDirectProperty, 6);
+            target.Bind(Class1.NonValidatedProperty, source);
+            source.OnNext(6);
+            source.OnNext(BindingValue<int>.BindingError(new Exception()));
+            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
+            source.OnNext(6);
 
             Assert.Empty(target.Notifications);
         }
 
         [Fact]
-        public void Setting_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation()
+        public void Binding_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation()
         {
             var target = new Class1();
+            var source = new Subject<BindingValue<int>>();
 
-            target.SetValue(Class1.NonValidatedDirectProperty, 6);
-
-            Assert.Empty(target.Notifications);
-        }
-
-        [Fact]
-        public void Setting_Validated_Direct_Property_Calls_UpdateDataValidation()
-        {
-            var target = new Class1();
-
-            target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6));
-            target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
-            target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
-            target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7));
-
-            Assert.Equal(
-                new[]
-                {
-                    new BindingNotification(6),
-                    new BindingNotification(new Exception(), BindingErrorType.Error),
-                    new BindingNotification(new Exception(), BindingErrorType.DataValidationError),
-                    new BindingNotification(7),
-                },
-                target.Notifications.AsEnumerable());
-        }
-
-        [Fact]
-        public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
-        {
-            var source = new Subject<object>();
-            var target = new Class1
-            {
-                [!Class1.NonValidatedProperty] = source.ToBinding(),
-            };
-
-            source.OnNext(new BindingNotification(6));
-            source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
-            source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
-            source.OnNext(new BindingNotification(7));
+            target.Bind(Class1.NonValidatedDirectProperty, source);
+            source.OnNext(6);
+            source.OnNext(BindingValue<int>.BindingError(new Exception()));
+            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
+            source.OnNext(6);
 
             Assert.Empty(target.Notifications);
         }
@@ -71,26 +43,23 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation()
         {
-            var source = new Subject<object>();
-            var target = new Class1
-            {
-                [!Class1.ValidatedDirectIntProperty] = source.ToBinding(),
-            };
-
-            source.OnNext(new BindingNotification(6));
-            source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
-            source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
-            source.OnNext(new BindingNotification(7));
-
-            Assert.Equal(
-                new[]
-                {
-                    new BindingNotification(6),
-                    new BindingNotification(new Exception(), BindingErrorType.Error),
-                    new BindingNotification(new Exception(), BindingErrorType.DataValidationError),
-                    new BindingNotification(7),
-                },
-                target.Notifications.AsEnumerable());
+            var target = new Class1();
+            var source = new Subject<BindingValue<int>>();
+
+            target.Bind(Class1.ValidatedDirectIntProperty, source);
+            source.OnNext(6);
+            source.OnNext(BindingValue<int>.BindingError(new Exception()));
+            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
+            source.OnNext(7);
+
+            var result = target.Notifications.Cast<BindingValue<int>>().ToList();
+            Assert.Equal(4, result.Count);
+            Assert.Equal(BindingValueType.Value, result[0].Type);
+            Assert.Equal(6, result[0].Value);
+            Assert.Equal(BindingValueType.BindingError, result[1].Type);
+            Assert.Equal(BindingValueType.DataValidationError, result[2].Type);
+            Assert.Equal(BindingValueType.Value, result[3].Type);
+            Assert.Equal(7, result[3].Value);
         }
 
         [Fact]
@@ -171,11 +140,13 @@ namespace Avalonia.Base.UnitTests
                 set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); }
             }
 
-            public IList<BindingNotification> Notifications { get; } = new List<BindingNotification>();
+            public IList<object> Notifications { get; } = new List<object>();
 
-            protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification notification)
+            protected override void UpdateDataValidation<T>(
+                AvaloniaProperty<T> property,
+                BindingValue<T> value)
             {
-                Notifications.Add(notification);
+                Notifications.Add(value);
             }
         }
 

+ 153 - 19
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@@ -7,12 +7,9 @@ using System.ComponentModel;
 using System.Reactive.Subjects;
 using System.Threading;
 using System.Threading.Tasks;
-using Avalonia;
 using Avalonia.Data;
 using Avalonia.Logging;
 using Avalonia.Platform;
-using Avalonia.Threading;
-using Avalonia.Markup.Data;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -22,7 +19,7 @@ namespace Avalonia.Base.UnitTests
     public class AvaloniaObjectTests_Direct
     {
         [Fact]
-        public void GetValue_Gets_Value()
+        public void GetValue_Gets_Default_Value()
         {
             var target = new Class1();
 
@@ -109,6 +106,62 @@ namespace Avalonia.Base.UnitTests
             Assert.True(raised);
         }
 
+        [Fact]
+        public void Setting_Object_Property_To_UnsetValue_Reverts_To_Default_Value()
+        {
+            Class1 target = new Class1();
+
+            target.SetValue(Class1.FrankProperty, "newvalue");
+            target.SetValue(Class1.FrankProperty, AvaloniaProperty.UnsetValue);
+
+            Assert.Equal("Kups", target.GetValue(Class1.FrankProperty));
+        }
+
+        [Fact]
+        public void Setting_Object_Property_To_DoNothing_Does_Nothing()
+        {
+            Class1 target = new Class1();
+
+            target.SetValue(Class1.FrankProperty, "newvalue");
+            target.SetValue(Class1.FrankProperty, BindingOperations.DoNothing);
+
+            Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty));
+        }
+
+        [Fact]
+        public void Bind_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
+            bool raised = false;
+
+            target.PropertyChanged += (s, e) =>
+                raised = e.Property == Class1.FooProperty &&
+                         (string)e.OldValue == "initial" &&
+                         (string)e.NewValue == "newvalue" &&
+                         e.Priority == BindingPriority.LocalValue;
+
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("newvalue");
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void PropertyChanged_Not_Raised_When_Value_Unchanged()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
+            var raised = 0;
+
+            target.PropertyChanged += (s, e) => ++raised;
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("newvalue");
+            source.OnNext("newvalue");
+
+            Assert.Equal(1, raised);
+        }
+
         [Fact]
         public void SetValue_On_Unregistered_Property_Throws_Exception()
         {
@@ -117,6 +170,35 @@ namespace Avalonia.Base.UnitTests
             Assert.Throws<ArgumentException>(() => target.SetValue(Class1.BarProperty, "value"));
         }
 
+        [Fact]
+        public void ClearValue_Restores_Default_value()
+        {
+            var target = new Class1();
+
+            Assert.Equal("initial", target.GetValue(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void ClearValue_Raises_PropertyChanged()
+        {
+            Class1 target = new Class1();
+            var raised = 0;
+
+            target.SetValue(Class1.FooProperty, "newvalue");
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Same(target, s);
+                Assert.Equal(Class1.FooProperty, e.Property);
+                Assert.Equal("newvalue", (string)e.OldValue);
+                Assert.Equal("unset", (string)e.NewValue);
+                ++raised;
+            };
+
+            target.ClearValue(Class1.FooProperty);
+
+            Assert.Equal(1, raised);
+        }
+
         [Fact]
         public void GetObservable_Returns_Values()
         {
@@ -170,7 +252,7 @@ namespace Avalonia.Base.UnitTests
         }
 
         [Fact]
-        public void Bind_NonGeneric_Uses_UnsetValue()
+        public void Bind_NonGeneric_Accepts_UnsetValue()
         {
             var target = new Class1();
             var source = new Subject<object>();
@@ -194,7 +276,7 @@ namespace Avalonia.Base.UnitTests
 
             source.OnNext(45);
 
-            Assert.Null(target.Foo);
+            Assert.Equal("unset", target.Foo);
         }
 
         [Fact]
@@ -207,7 +289,7 @@ namespace Avalonia.Base.UnitTests
 
             source.OnNext("foo");
 
-            Assert.Equal(0, target.Baz);
+            Assert.Equal(-1, target.Baz);
         }
 
         [Fact]
@@ -358,31 +440,67 @@ namespace Avalonia.Base.UnitTests
             Assert.True(raised);
         }
 
+        [Fact]
+        public void Binding_Error_Reverts_To_Default_Value()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
+
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("initial");
+            source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo")));
+
+            Assert.Equal("unset", target.GetValue(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void Binding_Error_With_FallbackValue_Causes_Target_Update()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
+
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("initial");
+            source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo"), "bar"));
+
+            Assert.Equal("bar", target.GetValue(Class1.FooProperty));
+        }
+
         [Fact]
         public void DataValidationError_Does_Not_Cause_Target_Update()
         {
             var target = new Class1();
-            var source = new Subject<object>();
+            var source = new Subject<BindingValue<string>>();
 
             target.Bind(Class1.FooProperty, source);
             source.OnNext("initial");
-            source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.DataValidationError));
+            source.OnNext(BindingValue<string>.DataValidationError(new InvalidOperationException("Foo")));
 
             Assert.Equal("initial", target.GetValue(Class1.FooProperty));
         }
 
+        [Fact]
+        public void DataValidationError_With_FallbackValue_Causes_Target_Update()
+        {
+            var target = new Class1();
+            var source = new Subject<BindingValue<string>>();
+
+            target.Bind(Class1.FooProperty, source);
+            source.OnNext("initial");
+            source.OnNext(BindingValue<string>.DataValidationError(new InvalidOperationException("Foo"), "bar"));
+
+            Assert.Equal("bar", target.GetValue(Class1.FooProperty));
+        }
+
         [Fact]
         public void BindingError_With_FallbackValue_Causes_Target_Update()
         {
             var target = new Class1();
-            var source = new Subject<object>();
+            var source = new Subject<BindingValue<string>>();
 
             target.Bind(Class1.FooProperty, source);
             source.OnNext("initial");
-            source.OnNext(new BindingNotification(
-                new InvalidOperationException("Foo"),
-                BindingErrorType.Error,
-                "fallback"));
+            source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo"), "fallback"));
 
             Assert.Equal("fallback", target.GetValue(Class1.FooProperty));
         }
@@ -391,7 +509,7 @@ namespace Avalonia.Base.UnitTests
         public void Binding_To_Direct_Property_Logs_BindingError()
         {
             var target = new Class1();
-            var source = new Subject<object>();
+            var source = new Subject<BindingValue<string>>();
             var called = false;
 
             LogCallback checkLogMessage = (level, area, src, mt, pv) =>
@@ -412,7 +530,7 @@ namespace Avalonia.Base.UnitTests
             {
                 target.Bind(Class1.FooProperty, source);
                 source.OnNext("baz");
-                source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error));
+                source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Binding Error Message")));
             }
 
             Assert.True(called);
@@ -447,7 +565,8 @@ namespace Avalonia.Base.UnitTests
                 "foo",
                 o => "foo",
                 null,
-                new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay));
+                new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay),
+                false);
             var bar = foo.AddOwner<Class2>(o => "bar");
 
             Assert.Equal(BindingMode.TwoWay, bar.GetMetadata<Class1>().DefaultBindingMode);
@@ -461,7 +580,8 @@ namespace Avalonia.Base.UnitTests
                 "foo",
                 o => "foo",
                 null,
-                new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay));
+                new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay),
+                false);
             var bar = foo.AddOwner<Class2>(o => "bar", defaultBindingMode: BindingMode.OneWayToSource);
 
             Assert.Equal(BindingMode.TwoWay, bar.GetMetadata<Class1>().DefaultBindingMode);
@@ -527,10 +647,18 @@ namespace Avalonia.Base.UnitTests
                     o => o.DoubleValue,
                     (o, v) => o.DoubleValue = v);
 
+            public static readonly DirectProperty<Class1, object> FrankProperty =
+                AvaloniaProperty.RegisterDirect<Class1, object>(
+                    nameof(Frank),
+                    o => o.Frank,
+                    (o, v) => o.Frank = v,
+                    unsetValue: "Kups");
+
             private string _foo = "initial";
             private readonly string _bar = "bar";
             private int _baz = 5;
             private double _doubleValue;
+            private object _frank;
 
             public string Foo
             {
@@ -554,6 +682,12 @@ namespace Avalonia.Base.UnitTests
                 get { return _doubleValue; }
                 set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); }
             }
+
+            public object Frank
+            {
+                get { return _frank; }
+                set { SetAndRaise(FrankProperty, ref _frank, value); }
+            }
         }
 
         private class Class2 : AvaloniaObject
@@ -609,4 +743,4 @@ namespace Avalonia.Base.UnitTests
             }
         }
     }
-}
+}

+ 16 - 3
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Reactive.Subjects;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests
@@ -27,11 +28,23 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void GetValue_Returns_Set_Value()
         {
-            Class1 target = new Class1();
+            var target = new Class1();
+            var property = Class1.FooProperty;
+
+            target.SetValue(property, "newvalue");
+
+            Assert.Equal("newvalue", target.GetValue(property));
+        }
+
+        [Fact]
+        public void GetValue_Returns_Bound_Value()
+        {
+            var target = new Class1();
+            var property = Class1.FooProperty;
 
-            target.SetValue(Class1.FooProperty, "newvalue");
+            target.Bind(property, new BehaviorSubject<string>("newvalue"));
 
-            Assert.Equal("newvalue", target.GetValue(Class1.FooProperty));
+            Assert.Equal("newvalue", target.GetValue(property));
         }
 
         [Fact]

+ 87 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@@ -20,6 +20,27 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
         }
 
+        [Fact]
+        public void ClearValue_Raises_PropertyChanged()
+        {
+            Class1 target = new Class1();
+            var raised = 0;
+
+            target.SetValue(Class1.FooProperty, "newvalue");
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Same(target, s);
+                Assert.Equal(Class1.FooProperty, e.Property);
+                Assert.Equal("newvalue", (string)e.OldValue);
+                Assert.Equal("foodefault", (string)e.NewValue);
+                ++raised;
+            };
+
+            target.ClearValue(Class1.FooProperty);
+
+            Assert.Equal(1, raised);
+        }
+
         [Fact]
         public void SetValue_Sets_Value()
         {
@@ -59,6 +80,25 @@ namespace Avalonia.Base.UnitTests
             Assert.True(raised);
         }
 
+        [Fact]
+        public void SetValue_Style_Priority_Raises_PropertyChanged()
+        {
+            Class1 target = new Class1();
+            bool raised = false;
+
+            target.PropertyChanged += (s, e) =>
+            {
+                raised = s == target &&
+                         e.Property == Class1.FooProperty &&
+                         (string)e.OldValue == "foodefault" &&
+                         (string)e.NewValue == "newvalue";
+            };
+
+            target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Style);
+
+            Assert.True(raised);
+        }
+
         [Fact]
         public void SetValue_Doesnt_Raise_PropertyChanged_If_Value_Not_Changed()
         {
@@ -177,6 +217,28 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("three", target.GetValue(Class1.FooProperty));
         }
 
+        [Fact]
+        public void SetValue_Style_Doesnt_Override_LocalValue()
+        {
+            Class1 target = new Class1();
+
+            target.SetValue(Class1.FooProperty, "one", BindingPriority.LocalValue);
+            Assert.Equal("one", target.GetValue(Class1.FooProperty));
+            target.SetValue(Class1.FooProperty, "two", BindingPriority.Style);
+            Assert.Equal("one", target.GetValue(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void SetValue_LocalValue_Overrides_Style()
+        {
+            Class1 target = new Class1();
+
+            target.SetValue(Class1.FooProperty, "one", BindingPriority.Style);
+            Assert.Equal("one", target.GetValue(Class1.FooProperty));
+            target.SetValue(Class1.FooProperty, "two", BindingPriority.LocalValue);
+            Assert.Equal("two", target.GetValue(Class1.FooProperty));
+        }
+
         [Fact]
         public void Setting_UnsetValue_Reverts_To_Default_Value()
         {
@@ -188,10 +250,35 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
         }
 
+        [Fact]
+        public void Setting_Object_Property_To_UnsetValue_Reverts_To_Default_Value()
+        {
+            Class1 target = new Class1();
+
+            target.SetValue(Class1.FrankProperty, "newvalue");
+            target.SetValue(Class1.FrankProperty, AvaloniaProperty.UnsetValue);
+
+            Assert.Equal("Kups", target.GetValue(Class1.FrankProperty));
+        }
+
+        [Fact]
+        public void Setting_Object_Property_To_DoNothing_Does_Nothing()
+        {
+            Class1 target = new Class1();
+
+            target.SetValue(Class1.FrankProperty, "newvalue");
+            target.SetValue(Class1.FrankProperty, BindingOperations.DoNothing);
+
+            Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty));
+        }
+
         private class Class1 : AvaloniaObject
         {
             public static readonly StyledProperty<string> FooProperty =
                 AvaloniaProperty.Register<Class1, string>("Foo", "foodefault");
+
+            public static readonly StyledProperty<object> FrankProperty =
+                AvaloniaProperty.Register<Class1, object>("Frank", "Kups");
         }
 
         private class Class2 : Class1

+ 0 - 156
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

@@ -1,156 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Reactive.Subjects;
-using Xunit;
-
-namespace Avalonia.Base.UnitTests
-{
-    public class AvaloniaObjectTests_Validation
-    {
-        [Fact]
-        public void SetValue_Causes_Validation()
-        {
-            var target = new Class1();
-
-            target.SetValue(Class1.QuxProperty, 5);
-            Assert.Throws<ArgumentOutOfRangeException>(() => target.SetValue(Class1.QuxProperty, 25));
-            Assert.Equal(5, target.GetValue(Class1.QuxProperty));
-        }
-
-        [Fact]
-        public void SetValue_Causes_Coercion()
-        {
-            var target = new Class1();
-
-            target.SetValue(Class1.QuxProperty, 5);
-            Assert.Equal(5, target.GetValue(Class1.QuxProperty));
-            target.SetValue(Class1.QuxProperty, -5);
-            Assert.Equal(0, target.GetValue(Class1.QuxProperty));
-            target.SetValue(Class1.QuxProperty, 15);
-            Assert.Equal(10, target.GetValue(Class1.QuxProperty));
-        }
-
-        [Fact]
-        public void Revalidate_Causes_Recoercion()
-        {
-            var target = new Class1();
-
-            target.SetValue(Class1.QuxProperty, 7);
-            Assert.Equal(7, target.GetValue(Class1.QuxProperty));
-            target.MaxQux = 5;
-            target.Revalidate(Class1.QuxProperty);
-        }
-
-        [Fact]
-        public void Validation_Can_Be_Overridden()
-        {
-            var target = new Class2();
-            Assert.Throws<ArgumentOutOfRangeException>(() => target.SetValue(Class1.QuxProperty, 5));
-        }
-
-        [Fact]
-        public void Validation_Can_Be_Overridden_With_Null()
-        {
-            var target = new Class3();
-            target.SetValue(Class1.QuxProperty, 50);
-            Assert.Equal(50, target.GetValue(Class1.QuxProperty));
-        }
-
-        [Fact]
-        public void Binding_To_UnsetValue_Doesnt_Throw()
-        {
-            var target = new Class1();
-            var source = new Subject<object>();
-
-            target.Bind(Class1.QuxProperty, source);
-
-            source.OnNext(AvaloniaProperty.UnsetValue);
-        }
-
-        [Fact]
-        public void Attached_Property_Should_Be_Validated()
-        {
-            var target = new Class2();
-
-            target.SetValue(Class1.AttachedProperty, 15);
-            Assert.Equal(10, target.GetValue(Class1.AttachedProperty));
-        }
-
-        [Fact]
-        public void PropertyChanged_Event_Uses_Coerced_Value()
-        {
-            var inst = new Class1();
-            inst.PropertyChanged += (sender, e) =>
-            {
-                Assert.Equal(10, e.NewValue);
-            };
-
-            inst.SetValue(Class1.QuxProperty, 15);
-        }
-
-        private class Class1 : AvaloniaObject
-        {
-            public static readonly StyledProperty<int> QuxProperty =
-                AvaloniaProperty.Register<Class1, int>("Qux", validate: Validate);
-
-            public static readonly AttachedProperty<int> AttachedProperty =
-            AvaloniaProperty.RegisterAttached<Class1, Class2, int>("Attached", validate: Validate);
-
-            public Class1()
-            {
-                MaxQux = 10;
-                ErrorQux = 20;
-            }
-
-            public int MaxQux { get; set; }
-
-            public int ErrorQux { get; }
-
-            private static int Validate(Class1 instance, int value)
-            {
-                if (value > instance.ErrorQux)
-                {
-                    throw new ArgumentOutOfRangeException();
-                }
-
-                return Math.Min(Math.Max(value, 0), ((Class1)instance).MaxQux);
-            }
-
-            private static int Validate(Class2 instance, int value)
-            {
-                return Math.Min(value, 10);
-            }
-        }
-
-        private class Class2 : AvaloniaObject
-        {
-            public static readonly StyledProperty<int> QuxProperty =
-                Class1.QuxProperty.AddOwner<Class2>();
-
-            static Class2()
-            {
-                QuxProperty.OverrideValidation<Class2>(Validate);
-            }
-
-            private static int Validate(Class2 instance, int value)
-            {
-                if (value < 100)
-                {
-                    throw new ArgumentOutOfRangeException();
-                }
-
-                return value;
-            }
-        }
-
-        private class Class3 : Class2
-        {
-            static Class3()
-            {
-                QuxProperty.OverrideValidation<Class3>(null);
-            }
-        }
-    }
-}

+ 31 - 0
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@@ -140,6 +140,37 @@ namespace Avalonia.Base.UnitTests
             {
                 OverrideMetadata(typeof(T), metadata);
             }
+
+            internal override void NotifyInitialized(IAvaloniaObject o)
+            {
+                throw new NotImplementedException();
+            }
+
+            internal override IDisposable RouteBind(
+                IAvaloniaObject o,
+                IObservable<BindingValue<object>> source,
+                BindingPriority priority)
+            {
+                throw new NotImplementedException();
+            }
+
+            internal override object RouteGetValue(IAvaloniaObject o)
+            {
+                throw new NotImplementedException();
+            }
+
+            internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent)
+            {
+                throw new NotImplementedException();
+            }
+
+            internal override void RouteSetValue(
+                IAvaloniaObject o,
+                object value,
+                BindingPriority priority)
+            {
+                throw new NotImplementedException();
+            }
         }
 
         private class Class1 : AvaloniaObject

+ 33 - 33
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs

@@ -16,39 +16,39 @@ namespace Avalonia.Base.UnitTests.Data.Core
 {
     public class ExpressionObserverTests_DataValidation : IClassFixture<InvariantCultureFixture>
     {
-        [Fact]
-        public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
-        {
-            var data = new ExceptionTest { MustBePositive = 5 };
-            var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
-            var validationMessageFound = false;
-
-            observer.OfType<BindingNotification>()
-                .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
-                .Subscribe(_ => validationMessageFound = true);
-            observer.SetValue(-5);
-
-            Assert.False(validationMessageFound);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public void Exception_Validation_Sends_DataValidationError()
-        {
-            var data = new ExceptionTest { MustBePositive = 5 };
-            var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
-            var validationMessageFound = false;
-
-            observer.OfType<BindingNotification>()
-                .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
-                .Subscribe(_ => validationMessageFound = true);
-            observer.SetValue(-5);
-
-            Assert.True(validationMessageFound);
-
-            GC.KeepAlive(data);
-        }
+        ////[Fact]
+        ////public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
+        ////{
+        ////    var data = new ExceptionTest { MustBePositive = 5 };
+        ////    var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
+        ////    var validationMessageFound = false;
+
+        ////    observer.OfType<BindingNotification>()
+        ////        .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
+        ////        .Subscribe(_ => validationMessageFound = true);
+        ////    observer.SetValue(-5);
+
+        ////    Assert.False(validationMessageFound);
+
+        ////    GC.KeepAlive(data);
+        ////}
+
+        ////[Fact]
+        ////public void Exception_Validation_Sends_DataValidationError()
+        ////{
+        ////    var data = new ExceptionTest { MustBePositive = 5 };
+        ////    var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
+        ////    var validationMessageFound = false;
+
+        ////    observer.OfType<BindingNotification>()
+        ////        .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
+        ////        .Subscribe(_ => validationMessageFound = true);
+        ////    observer.SetValue(-5);
+
+        ////    Assert.True(validationMessageFound);
+
+        ////    GC.KeepAlive(data);
+        ////}
 
         [Fact]
         public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()

+ 3 - 13
tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs

@@ -34,8 +34,9 @@ namespace Avalonia.Base.UnitTests
             var target = new DirectProperty<Class1, string>(
                 "test", 
                 o => null, 
-                null, 
-                new DirectPropertyMetadata<string>());
+                null,
+                new DirectPropertyMetadata<string>(),
+                false);
 
             Assert.True(target.IsDirect);
         }
@@ -71,17 +72,6 @@ namespace Avalonia.Base.UnitTests
             Assert.Same(p1.Initialized, p2.Initialized);
         }
 
-        [Fact]
-        public void IsAnimating_On_DirectProperty_With_Binding_Returns_False()
-        {
-            var target = new Class1();
-            var source = new BehaviorSubject<string>("foo");
-
-            target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
-
-            Assert.False(target.IsAnimating(Class1.FooProperty));
-        }
-
         private class Class1 : AvaloniaObject
         {
             public static readonly DirectProperty<Class1, string> FooProperty =

+ 148 - 223
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@@ -1,314 +1,239 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using Avalonia.Utilities;
-using Moq;
-using System;
+using System;
 using System.Linq;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
+using System.Reactive.Disposables;
+using Avalonia.Data;
+using Avalonia.PropertyStore;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests
 {
     public class PriorityValueTests
     {
-        private static readonly AvaloniaProperty TestProperty = 
-            new StyledProperty<string>(
-                "Test", 
-                typeof(PriorityValueTests), 
-                new StyledPropertyMetadata<string>());
+        private static readonly IValueSink NullSink = Mock.Of<IValueSink>();
+        private static readonly IAvaloniaObject Owner = Mock.Of<IAvaloniaObject>();
+        private static readonly StyledProperty<string> TestProperty = new StyledProperty<string>(
+            "Test",
+            typeof(PriorityValueTests),
+            new StyledPropertyMetadata<string>());
 
         [Fact]
-        public void Initial_Value_Should_Be_UnsetValue()
+        public void Constructor_Should_Set_Value_Based_On_Initial_Entry()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
+            var target = new PriorityValue<string>(
+                Owner,
+                TestProperty,
+                NullSink,
+                new ConstantValueEntry<string>(TestProperty, "1", BindingPriority.StyleTrigger));
 
-            Assert.Same(AvaloniaProperty.UnsetValue, target.Value);
+            Assert.Equal("1", target.Value.Value);
+            Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority);
         }
 
         [Fact]
-        public void First_Binding_Sets_Value()
+        public void SetValue_LocalValue_Should_Not_Add_Entries()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
+            var target = new PriorityValue<string>(
+                Owner,
+                TestProperty,
+                NullSink);
 
-            target.Add(Single("foo"), 0);
+            target.SetValue("1", BindingPriority.LocalValue);
+            target.SetValue("2", BindingPriority.LocalValue);
 
-            Assert.Equal("foo", target.Value);
+            Assert.Empty(target.Entries);
         }
 
         [Fact]
-        public void Changing_Binding_Should_Set_Value()
+        public void SetValue_Non_LocalValue_Should_Add_Entries()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var subject = new BehaviorSubject<string>("foo");
+            var target = new PriorityValue<string>(
+                Owner,
+                TestProperty,
+                NullSink);
 
-            target.Add(subject, 0);
-            Assert.Equal("foo", target.Value);
-            subject.OnNext("bar");
-            Assert.Equal("bar", target.Value);
-        }
+            target.SetValue("1", BindingPriority.Style);
+            target.SetValue("2", BindingPriority.Animation);
 
-        [Fact]
-        public void Setting_Direct_Value_Should_Override_Binding()
-        {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
+            var result = target.Entries
+                .OfType<ConstantValueEntry<string>>()
+                .Select(x => x.Value.Value)
+                .ToList();
 
-            target.Add(Single("foo"), 0);
-            target.SetValue("bar", 0);
-
-            Assert.Equal("bar", target.Value);
+            Assert.Equal(new[] { "1", "2" }, result);
         }
 
         [Fact]
-        public void Binding_Firing_Should_Override_Direct_Value()
+        public void Binding_With_Same_Priority_Should_Be_Appended()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var source = new BehaviorSubject<object>("initial");
-
-            target.Add(source, 0);
-            Assert.Equal("initial", target.Value);
-            target.SetValue("first", 0);
-            Assert.Equal("first", target.Value);
-            source.OnNext("second");
-            Assert.Equal("second", target.Value);
-        }
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
+            var source2 = new Source("2");
 
-        [Fact]
-        public void Earlier_Binding_Firing_Should_Not_Override_Later()
-        {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var nonActive = new BehaviorSubject<object>("na");
-            var source = new BehaviorSubject<object>("initial");
-
-            target.Add(nonActive, 1);
-            target.Add(source, 1);
-            Assert.Equal("initial", target.Value);
-            target.SetValue("first", 1);
-            Assert.Equal("first", target.Value);
-            nonActive.OnNext("second");
-            Assert.Equal("first", target.Value);
-        }
+            target.AddBinding(source1, BindingPriority.LocalValue);
+            target.AddBinding(source2, BindingPriority.LocalValue);
 
-        [Fact]
-        public void Binding_Completing_Should_Revert_To_Direct_Value()
-        {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var source = new BehaviorSubject<object>("initial");
-
-            target.Add(source, 0);
-            Assert.Equal("initial", target.Value);
-            target.SetValue("first", 0);
-            Assert.Equal("first", target.Value);
-            source.OnNext("second");
-            Assert.Equal("second", target.Value);
-            source.OnCompleted();
-            Assert.Equal("first", target.Value);
-        }
-
-        [Fact]
-        public void Binding_With_Lower_Priority_Has_Precedence()
-        {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-
-            target.Add(Single("foo"), 1);
-            target.Add(Single("bar"), 0);
-            target.Add(Single("baz"), 1);
+            var result = target.Entries
+                .OfType<BindingEntry<string>>()
+                .Select(x => x.Source)
+                .OfType<Source>()
+                .Select(x => x.Id)
+                .ToList();
 
-            Assert.Equal("bar", target.Value);
+            Assert.Equal(new[] { "1", "2" }, result);
         }
 
         [Fact]
-        public void Later_Binding_With_Same_Priority_Should_Take_Precedence()
+        public void Binding_With_Higher_Priority_Should_Be_Appended()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
+            var source2 = new Source("2");
 
-            target.Add(Single("foo"), 1);
-            target.Add(Single("bar"), 0);
-            target.Add(Single("baz"), 0);
-            target.Add(Single("qux"), 1);
+            target.AddBinding(source1, BindingPriority.LocalValue);
+            target.AddBinding(source2, BindingPriority.Animation);
 
-            Assert.Equal("baz", target.Value);
-        }
+            var result = target.Entries
+                .OfType<BindingEntry<string>>()
+                .Select(x => x.Source)
+                .OfType<Source>()
+                .Select(x => x.Id)
+                .ToList();
 
-        [Fact]
-        public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value()
-        {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var subject = new BehaviorSubject<string>("bar");
-
-            target.Add(Single("foo"), 0);
-            target.Add(subject, 1);
-            Assert.Equal("foo", target.Value);
-            subject.OnNext("baz");
-            Assert.Equal("foo", target.Value);
+            Assert.Equal(new[] { "1", "2" }, result);
         }
 
         [Fact]
-        public void UnsetValue_Should_Fall_Back_To_Next_Binding()
+        public void Binding_With_Lower_Priority_Should_Be_Prepended()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var subject = new BehaviorSubject<object>("bar");
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
+            var source2 = new Source("2");
 
-            target.Add(subject, 0);
-            target.Add(Single("foo"), 1);
+            target.AddBinding(source1, BindingPriority.LocalValue);
+            target.AddBinding(source2, BindingPriority.Style);
 
-            Assert.Equal("bar", target.Value);
+            var result = target.Entries
+                .OfType<BindingEntry<string>>()
+                .Select(x => x.Source)
+                .OfType<Source>()
+                .Select(x => x.Id)
+                .ToList();
 
-            subject.OnNext(AvaloniaProperty.UnsetValue);
-
-            Assert.Equal("foo", target.Value);
+            Assert.Equal(new[] { "2", "1" }, result);
         }
 
         [Fact]
-        public void Adding_Value_Should_Call_OnNext()
+        public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle()
         {
-            var owner = GetMockOwner();
-            var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
-
-            target.Add(Single("foo"), 0);
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
+            var source2 = new Source("2");
+            var source3 = new Source("3");
 
-            owner.Verify(x => x.Changed(target.Property, target.ValuePriority, AvaloniaProperty.UnsetValue, "foo"));
-        }
-
-        [Fact]
-        public void Changing_Value_Should_Call_OnNext()
-        {
-            var owner = GetMockOwner();
-            var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
-            var subject = new BehaviorSubject<object>("foo");
+            target.AddBinding(source1, BindingPriority.LocalValue);
+            target.AddBinding(source2, BindingPriority.Style);
+            target.AddBinding(source3, BindingPriority.Style);
 
-            target.Add(subject, 0);
-            subject.OnNext("bar");
+            var result = target.Entries
+                .OfType<BindingEntry<string>>()
+                .Select(x => x.Source)
+                .OfType<Source>()
+                .Select(x => x.Id)
+                .ToList();
 
-            owner.Verify(x => x.Changed(target.Property, target.ValuePriority, "foo", "bar"));
+            Assert.Equal(new[] { "2", "3", "1" }, result);
         }
 
         [Fact]
-        public void Disposing_A_Binding_Should_Revert_To_Next_Value()
+        public void Competed_Binding_Should_Be_Removed()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-
-            target.Add(Single("foo"), 0);
-            var disposable = target.Add(Single("bar"), 0);
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
+            var source2 = new Source("2");
+            var source3 = new Source("3");
 
-            Assert.Equal("bar", target.Value);
-            disposable.Dispose();
-            Assert.Equal("foo", target.Value);
-        }
-
-        [Fact]
-        public void Disposing_A_Binding_Should_Remove_BindingEntry()
-        {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
+            target.AddBinding(source1, BindingPriority.LocalValue).Start();
+            target.AddBinding(source2, BindingPriority.Style).Start();
+            target.AddBinding(source3, BindingPriority.Style).Start();
+            source3.OnCompleted();
 
-            target.Add(Single("foo"), 0);
-            var disposable = target.Add(Single("bar"), 0);
+            var result = target.Entries
+                .OfType<BindingEntry<string>>()
+                .Select(x => x.Source)
+                .OfType<Source>()
+                .Select(x => x.Id)
+                .ToList();
 
-            Assert.Equal(2, target.GetBindings().Count());
-            disposable.Dispose();
-            Assert.Single(target.GetBindings());
+            Assert.Equal(new[] { "2", "1" }, result);
         }
 
         [Fact]
-        public void Completing_A_Binding_Should_Revert_To_Previous_Binding()
+        public void Value_Should_Come_From_Last_Entry()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var source = new BehaviorSubject<object>("bar");
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
+            var source2 = new Source("2");
+            var source3 = new Source("3");
 
-            target.Add(Single("foo"), 0);
-            target.Add(source, 0);
+            target.AddBinding(source1, BindingPriority.LocalValue).Start();
+            target.AddBinding(source2, BindingPriority.Style).Start();
+            target.AddBinding(source3, BindingPriority.Style).Start();
 
-            Assert.Equal("bar", target.Value);
-            source.OnCompleted();
-            Assert.Equal("foo", target.Value);
+            Assert.Equal("1", target.Value.Value);
         }
 
         [Fact]
-        public void Completing_A_Binding_Should_Revert_To_Lower_Priority()
+        public void LocalValue_Should_Override_LocalValue_Binding()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var source = new BehaviorSubject<object>("bar");
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
 
-            target.Add(Single("foo"), 1);
-            target.Add(source, 0);
+            target.AddBinding(source1, BindingPriority.LocalValue).Start();
+            target.SetValue("2", BindingPriority.LocalValue);
 
-            Assert.Equal("bar", target.Value);
-            source.OnCompleted();
-            Assert.Equal("foo", target.Value);
+            Assert.Equal("2", target.Value.Value);
         }
 
         [Fact]
-        public void Completing_A_Binding_Should_Remove_BindingEntry()
+        public void LocalValue_Should_Override_Style_Binding()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
-            var subject = new BehaviorSubject<object>("bar");
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
 
-            target.Add(Single("foo"), 0);
-            target.Add(subject, 0);
+            target.AddBinding(source1, BindingPriority.Style).Start();
+            target.SetValue("2", BindingPriority.LocalValue);
 
-            Assert.Equal(2, target.GetBindings().Count());
-            subject.OnCompleted();
-            Assert.Single(target.GetBindings());
+            Assert.Equal("2", target.Value.Value);
         }
 
         [Fact]
-        public void Direct_Value_Should_Be_Coerced()
+        public void LocalValue_Should_Not_Override_Animation_Binding()
         {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
+            var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
+            var source1 = new Source("1");
 
-            target.SetValue(5, 0);
-            Assert.Equal(5, target.Value);
-            target.SetValue(15, 0);
-            Assert.Equal(10, target.Value);
-        }
+            target.AddBinding(source1, BindingPriority.Animation).Start();
+            target.SetValue("2", BindingPriority.LocalValue);
 
-        [Fact]
-        public void Bound_Value_Should_Be_Coerced()
-        {
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
-            var source = new Subject<object>();
-
-            target.Add(source, 0);
-            source.OnNext(5);
-            Assert.Equal(5, target.Value);
-            source.OnNext(15);
-            Assert.Equal(10, target.Value);
+            Assert.Equal("1", target.Value.Value);
         }
 
-        [Fact]
-        public void Revalidate_Should_ReCoerce_Value()
+        private class Source : IObservable<BindingValue<string>>
         {
-            var max = 10;
-            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, max));
-            var source = new Subject<object>();
-
-            target.Add(source, 0);
-            source.OnNext(5);
-            Assert.Equal(5, target.Value);
-            source.OnNext(15);
-            Assert.Equal(10, target.Value);
-            max = 12;
-            target.Revalidate();
-            Assert.Equal(12, target.Value);
-        }
+            private IObserver<BindingValue<string>> _observer;
 
-        /// <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)
-        {
-            return Observable.Never<T>().StartWith(value);
-        }
+            public Source(string id) => Id = id;
+            public string Id { get; }
 
-        private static Mock<IPriorityValueOwner> GetMockOwner()
-        {
-            var owner = new Mock<IPriorityValueOwner>();
-            owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny<AvaloniaProperty>())).Returns(new DeferredSetter<object>());
-            return owner;
+            public IDisposable Subscribe(IObserver<BindingValue<string>> observer)
+            {
+                _observer = observer;
+                observer.OnNext(Id);
+                return Disposable.Empty;
+            }
+
+            public void OnCompleted() => _observer.OnCompleted();
         }
     }
 }

+ 1 - 1
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@@ -780,7 +780,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
             public OldDataContextTest()
             {
-                Bind(BarProperty, this.GetObservable(FooProperty));
+                this.Bind(BarProperty, this.GetObservable(FooProperty));
             }
         }
 

+ 3 - 3
tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs

@@ -35,7 +35,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
             target.Verify(x => x.Bind(
                 TextBox.TextProperty, 
-                It.IsAny<IObservable<object>>(), 
+                It.IsAny<IObservable<BindingValue<object>>>(), 
                 BindingPriority.TemplatedParent));
         }
 
@@ -55,7 +55,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
             target.Verify(x => x.Bind(
                 TextBox.TextProperty,
-                It.IsAny<ISubject<object>>(),
+                It.IsAny<IObservable<BindingValue<object>>>(),
                 BindingPriority.TemplatedParent));
         }
 
@@ -68,7 +68,7 @@ namespace Avalonia.Markup.UnitTests.Data
             result.Setup(x => x.GetValue(Control.TemplatedParentProperty)).Returns(templatedParent);
             result.Setup(x => x.GetValue((AvaloniaProperty)Control.TemplatedParentProperty)).Returns(templatedParent);
             result.Setup(x => x.GetValue((AvaloniaProperty)TextBox.TextProperty)).Returns(text);
-            result.Setup(x => x.Bind(It.IsAny<AvaloniaProperty>(), It.IsAny<IObservable<object>>(), It.IsAny<BindingPriority>()))
+            result.Setup(x => x.Bind(It.IsAny<AvaloniaProperty>(), It.IsAny<IObservable<BindingValue<object>>>(), It.IsAny<BindingPriority>()))
                 .Returns(Disposable.Empty);
             return result;
         }

+ 4 - 3
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using Avalonia.Controls;
+using Avalonia.Data;
 using Avalonia.LogicalTree;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -20,10 +21,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             base.OnAttachedToLogicalTree(e);
         }
 
-        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
         {
-            Order.Add($"Property {e.Property.Name} Changed");
-            base.OnPropertyChanged(e);
+            Order.Add($"Property {property.Name} Changed");
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
         }
 
         void ISupportInitialize.BeginInit()

+ 32 - 2
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@@ -131,7 +131,7 @@ namespace Avalonia.Styling.UnitTests
                 throw new NotImplementedException();
             }
 
-            public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority)
+            public IDisposable Bind(AvaloniaProperty property, IObservable<BindingValue<object>> source, BindingPriority priority)
             {
                 throw new NotImplementedException();
             }
@@ -146,7 +146,7 @@ namespace Avalonia.Styling.UnitTests
                 throw new NotImplementedException();
             }
 
-            public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
+            public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<BindingValue<T>> source, BindingPriority priority = BindingPriority.LocalValue)
             {
                 throw new NotImplementedException();
             }
@@ -165,6 +165,36 @@ namespace Avalonia.Styling.UnitTests
             {
                 throw new NotImplementedException();
             }
+
+            public void ClearValue(AvaloniaProperty property)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void ClearValue<T>(AvaloniaProperty<T> property)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void AddInheritanceChild(IAvaloniaObject child)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void RemoveInheritanceChild(IAvaloniaObject child)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void InheritanceParentChanged<T>(StyledPropertyBase<T> property, IAvaloniaObject oldParent, IAvaloniaObject newParent)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void InheritedPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, Optional<T> newValue)
+            {
+                throw new NotImplementedException();
+            }
         }
 
         public class TestLogical1 : TestLogical

+ 32 - 2
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@@ -161,7 +161,7 @@ namespace Avalonia.Styling.UnitTests
                 throw new NotImplementedException();
             }
 
-            public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
+            public IDisposable Bind(AvaloniaProperty property, IObservable<BindingValue<object>> source, BindingPriority priority = BindingPriority.LocalValue)
             {
                 throw new NotImplementedException();
             }
@@ -176,7 +176,7 @@ namespace Avalonia.Styling.UnitTests
                 throw new NotImplementedException();
             }
 
-            public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
+            public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<BindingValue<T>> source, BindingPriority priority = BindingPriority.LocalValue)
             {
                 throw new NotImplementedException();
             }
@@ -195,6 +195,36 @@ namespace Avalonia.Styling.UnitTests
             {
                 throw new NotImplementedException();
             }
+
+            public void ClearValue(AvaloniaProperty property)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void ClearValue<T>(AvaloniaProperty<T> property)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void AddInheritanceChild(IAvaloniaObject child)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void RemoveInheritanceChild(IAvaloniaObject child)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void InheritanceParentChanged<T>(StyledPropertyBase<T> property, IAvaloniaObject oldParent, IAvaloniaObject newParent)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void InheritedPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, Optional<T> newValue)
+            {
+                throw new NotImplementedException();
+            }
         }
 
         public class TestLogical1 : TestLogical

+ 4 - 4
tests/Avalonia.Styling.UnitTests/SetterTests.cs

@@ -83,7 +83,7 @@ namespace Avalonia.Styling.UnitTests
 
             control.Verify(x => x.Bind(
                 TextBlock.TextProperty,
-                It.IsAny<IObservable<object>>(),
+                It.IsAny<IObservable<BindingValue<object>>>(),
                 BindingPriority.Style));
         }
 
@@ -99,7 +99,7 @@ namespace Avalonia.Styling.UnitTests
 
             control.Verify(x => x.Bind(
                 TextBlock.TextProperty,
-                It.IsAny<IObservable<object>>(),
+                It.IsAny<IObservable<BindingValue<object>>>(),
                 BindingPriority.StyleTrigger));
         }
 
@@ -114,7 +114,7 @@ namespace Avalonia.Styling.UnitTests
 
             control.Verify(x => x.Bind(
                 TextBlock.TextProperty,
-                It.IsAny<IObservable<object>>(),
+                It.IsAny<IObservable<BindingValue<object>>>(),
                 BindingPriority.Style));
         }
 
@@ -130,7 +130,7 @@ namespace Avalonia.Styling.UnitTests
 
             control.Verify(x => x.Bind(
                 TextBlock.TextProperty,
-                It.IsAny<IObservable<object>>(),
+                It.IsAny<IObservable<BindingValue<object>>>(),
                 BindingPriority.StyleTrigger));
         }
 

+ 32 - 2
tests/Avalonia.Styling.UnitTests/TestControlBase.cs

@@ -70,12 +70,42 @@ namespace Avalonia.Styling.UnitTests
             throw new NotImplementedException();
         }
 
-        public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
+        public IDisposable Bind(AvaloniaProperty property, IObservable<BindingValue<object>> source, BindingPriority priority = BindingPriority.LocalValue)
         {
             throw new NotImplementedException();
         }
 
-        public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
+        public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<BindingValue<T>> source, BindingPriority priority = BindingPriority.LocalValue)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void ClearValue(AvaloniaProperty property)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void ClearValue<T>(AvaloniaProperty<T> property)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void AddInheritanceChild(IAvaloniaObject child)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void RemoveInheritanceChild(IAvaloniaObject child)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void InheritanceParentChanged<T>(StyledPropertyBase<T> property, IAvaloniaObject oldParent, IAvaloniaObject newParent)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void InheritedPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, Optional<T> newValue)
         {
             throw new NotImplementedException();
         }

+ 32 - 2
tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

@@ -58,12 +58,12 @@ namespace Avalonia.Styling.UnitTests
             throw new NotImplementedException();
         }
 
-        public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
+        public IDisposable Bind(AvaloniaProperty property, IObservable<BindingValue<object>> source, BindingPriority priority = BindingPriority.LocalValue)
         {
             throw new NotImplementedException();
         }
 
-        public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
+        public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<BindingValue<T>> source, BindingPriority priority = BindingPriority.LocalValue)
         {
             throw new NotImplementedException();
         }
@@ -77,5 +77,35 @@ namespace Avalonia.Styling.UnitTests
         {
             throw new NotImplementedException();
         }
+
+        public void ClearValue(AvaloniaProperty property)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void ClearValue<T>(AvaloniaProperty<T> property)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void AddInheritanceChild(IAvaloniaObject child)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void RemoveInheritanceChild(IAvaloniaObject child)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void InheritanceParentChanged<T>(StyledPropertyBase<T> property, IAvaloniaObject oldParent, IAvaloniaObject newParent)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void InheritedPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, Optional<T> newValue)
+        {
+            throw new NotImplementedException();
+        }
     }
 }