diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs
index ca45fb8c4d4..cc1ac8ded65 100644
--- a/src/Avalonia.Animation/Animatable.cs
+++ b/src/Avalonia.Animation/Animatable.cs
@@ -65,26 +65,30 @@ public Transitions Transitions
}
}
- ///
- /// Reacts to a change in a value in
- /// order to animate the change if a is set for the property.
- ///
- /// The event args.
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ protected override void OnPropertyChanged(
+ AvaloniaProperty property,
+ Optional oldValue,
+ BindingValue 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.GetValueOrDefault(),
+ newValue.GetValueOrDefault());
- _previousTransitions[e.Property] = instance;
+ _previousTransitions[property] = instance;
return;
}
}
diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs
index fdb04b6dfc8..d1df5fa5e36 100644
--- a/src/Avalonia.Base/AttachedProperty.cs
+++ b/src/Avalonia.Base/AttachedProperty.cs
@@ -18,12 +18,14 @@ public class AttachedProperty : StyledProperty
/// The class that is registering the property.
/// The property metadata.
/// Whether the property inherits its value.
+ /// A value validation callback.
public AttachedProperty(
string name,
- Type ownerType,
+ Type ownerType,
StyledPropertyMetadata metadata,
- bool inherits = false)
- : base(name, ownerType, metadata, inherits)
+ bool inherits = false,
+ Func validate = null)
+ : base(name, ownerType, metadata, inherits, validate)
{
}
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 0499907ab82..6a00feaf79f 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -4,13 +4,11 @@
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;
namespace Avalonia
{
@@ -20,13 +18,13 @@ namespace Avalonia
///
/// This class is analogous to DependencyObject in WPF.
///
- public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
+ public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
{
private IAvaloniaObject _inheritanceParent;
- private List _directBindings;
+ private List _directBindings;
private PropertyChangedEventHandler _inpcChanged;
private EventHandler _propertyChanged;
- private EventHandler _inheritablePropertyChanged;
+ private List _inheritanceChildren;
private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
@@ -57,15 +55,6 @@ event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
remove { _inpcChanged -= value; }
}
- ///
- /// Raised when an inheritable value changes on this object.
- ///
- event EventHandler IAvaloniaObject.InheritablePropertyChanged
- {
- add { _inheritablePropertyChanged += value; }
- remove { _inheritablePropertyChanged -= value; }
- }
-
///
/// Gets or sets the parent object that inherited values
/// are inherited from.
@@ -83,47 +72,27 @@ protected IAvaloniaObject InheritanceParent
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);
}
}
}
@@ -166,10 +135,56 @@ public IBinding this[IndexerDescriptor binding]
/// The property.
public void ClearValue(AvaloniaProperty property)
{
- Contract.Requires(property != null);
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
+ property.RouteClearValue(this);
+ }
+
+ ///
+ /// Clears a 's local value.
+ ///
+ /// The property.
+ public void ClearValue(AvaloniaProperty property)
+ {
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ VerifyAccess();
+
+ switch (property)
+ {
+ case StyledPropertyBase styled:
+ ClearValue(styled);
+ break;
+ case DirectPropertyBase direct:
+ ClearValue(direct);
+ break;
+ default:
+ throw new NotSupportedException("Unsupported AvaloniaProperty type.");
+ }
+ }
+
+ ///
+ /// Clears a 's local value.
+ ///
+ /// The property.
+ public void ClearValue(StyledPropertyBase property)
+ {
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ VerifyAccess();
+
+ _values?.ClearLocalValue(property);
+ }
+
+ ///
+ /// Clears a 's local value.
+ ///
+ /// The property.
+ public void ClearValue(DirectPropertyBase property)
+ {
+ property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
- SetValue(property, AvaloniaProperty.UnsetValue);
+ var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
+ p.InvokeSetter(this, p.GetUnsetValue(GetType()));
}
///
@@ -210,21 +225,23 @@ public void ClearValue(AvaloniaProperty property)
/// The value.
public object GetValue(AvaloniaProperty property)
{
- if (property is null)
- {
- throw new ArgumentNullException(nameof(property));
- }
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ return property.RouteGetValue(this);
+ }
+
+ ///
+ /// Gets a value.
+ ///
+ /// The type of the property.
+ /// The property.
+ /// The value.
+ public T GetValue(StyledPropertyBase 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);
}
///
@@ -233,14 +250,13 @@ public object GetValue(AvaloniaProperty property)
/// The type of the property.
/// The property.
/// The value.
- public T GetValue(AvaloniaProperty property)
+ public T GetValue(DirectPropertyBase 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);
}
///
@@ -284,16 +300,43 @@ public void SetValue(
object value,
BindingPriority priority = BindingPriority.LocalValue)
{
- Contract.Requires(property != null);
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
+ property.RouteSetValue(this, value, priority);
+ }
+
+ ///
+ /// Sets a value.
+ ///
+ /// The type of the property.
+ /// The property.
+ /// The value.
+ /// The priority of the value.
+ public void SetValue(
+ StyledPropertyBase property,
+ T value,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
- if (property.IsDirect)
+ LogPropertySet(property, value, priority);
+
+ if (value is UnsetValueType)
{
- SetDirectValue(property, value);
+ if (priority == BindingPriority.LocalValue)
+ {
+ Values.ClearLocalValue(property);
+ }
+ else
+ {
+ throw new NotSupportedException(
+ "Cannot set property to Unset at non-local value priority.");
+ }
}
- else
+ else if (!(value is DoNothingType))
{
- SetStyledValue(property, value, priority);
+ Values.SetValue(property, value, priority);
}
}
@@ -303,69 +346,35 @@ public void SetValue(
/// The type of the property.
/// The property.
/// The value.
- /// The priority of the value.
- public void SetValue(
- AvaloniaProperty property,
- T value,
- BindingPriority priority = BindingPriority.LocalValue)
+ public void SetValue(DirectPropertyBase property, T value)
{
- Contract.Requires(property != null);
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ VerifyAccess();
- SetValue((AvaloniaProperty)property, value, priority);
+ LogPropertySet(property, value, BindingPriority.LocalValue);
+ SetDirectValueUnchecked(property, value);
}
///
/// Binds a to an observable.
///
+ /// The type of the property.
/// The property.
/// The observable.
/// The priority of the binding.
///
/// A disposable which can be used to terminate the binding.
///
- public IDisposable Bind(
- AvaloniaProperty property,
- IObservable
/// The property being bound.
- /// The binding notification.
- private void LogIfError(AvaloniaProperty property, BindingNotification notification)
+ /// The binding notification.
+ private void LogIfError(AvaloniaProperty property, BindingValue 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 +791,7 @@ private void LogIfError(AvaloniaProperty property, BindingNotification notificat
}
else
{
- LogBindingError(property, notification.Error);
+ LogBindingError(property, value.Error);
}
}
}
@@ -814,7 +802,7 @@ private void LogIfError(AvaloniaProperty property, BindingNotification notificat
/// The property.
/// The new value.
/// The priority.
- private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority)
+ private void LogPropertySet(AvaloniaProperty property, T value, BindingPriority priority)
{
Logger.TryGet(LogEventLevel.Verbose)?.Log(
LogArea.Property,
@@ -825,16 +813,16 @@ private void LogPropertySet(AvaloniaProperty property, object value, BindingPrio
priority);
}
- private class DirectBindingSubscription : IObserver, IDisposable
+ private class DirectBindingSubscription : IObserver>, IDisposable
{
- readonly AvaloniaObject _owner;
- readonly AvaloniaProperty _property;
- IDisposable _subscription;
+ private readonly AvaloniaObject _owner;
+ private readonly DirectPropertyBase _property;
+ private readonly IDisposable _subscription;
public DirectBindingSubscription(
AvaloniaObject owner,
- AvaloniaProperty property,
- IObservable source)
+ DirectPropertyBase property,
+ IObservable> source)
{
_owner = owner;
_property = property;
@@ -850,11 +838,22 @@ public void Dispose()
public void OnCompleted() => Dispose();
public void OnError(Exception error) => Dispose();
-
- public void OnNext(object value)
+ public void OnNext(BindingValue 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));
+ }
}
}
}
diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
index ad1cefd4ea4..a4c7fa95a50 100644
--- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs
+++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
@@ -68,6 +68,51 @@ public static IObservable GetObservable(this IAvaloniaObject o, AvaloniaPr
return new AvaloniaPropertyObservable(o, property);
}
+ ///
+ /// Gets an observable for a .
+ ///
+ /// The object.
+ /// The property.
+ ///
+ /// An observable which fires immediately with the current value of the property on the
+ /// object and subsequently each time the property value changes.
+ ///
+ ///
+ /// The subscription to is created using a weak reference.
+ ///
+ public static IObservable> GetBindingObservable(
+ this IAvaloniaObject o,
+ AvaloniaProperty property)
+ {
+ Contract.Requires(o != null);
+ Contract.Requires(property != null);
+
+ return new AvaloniaPropertyBindingObservable(o, property);
+ }
+
+ ///
+ /// Gets an observable for a .
+ ///
+ /// The object.
+ /// The property type.
+ /// The property.
+ ///
+ /// An observable which fires immediately with the current value of the property on the
+ /// object and subsequently each time the property value changes.
+ ///
+ ///
+ /// The subscription to is created using a weak reference.
+ ///
+ public static IObservable> GetBindingObservable(
+ this IAvaloniaObject o,
+ AvaloniaProperty property)
+ {
+ Contract.Requires(o != null);
+ Contract.Requires(property != null);
+
+ return new AvaloniaPropertyBindingObservable(o, property);
+ }
+
///
/// Gets an observable that listens for property changed events for an
/// .
@@ -80,7 +125,7 @@ public static IObservable GetObservable(this IAvaloniaObject o, AvaloniaPr
/// for the specified property.
///
public static IObservable GetPropertyChangedObservable(
- this IAvaloniaObject o,
+ this IAvaloniaObject o,
AvaloniaProperty property)
{
Contract.Requires(o != null);
@@ -134,6 +179,167 @@ public static ISubject GetSubject(
o.GetObservable(property));
}
+ ///
+ /// Gets a subject for a .
+ ///
+ /// The object.
+ /// The property.
+ ///
+ /// The priority with which binding values are written to the object.
+ ///
+ ///
+ /// An which can be used for two-way binding to/from the
+ /// property.
+ ///
+ public static ISubject> GetBindingSubject(
+ this IAvaloniaObject o,
+ AvaloniaProperty property,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ return Subject.Create>(
+ Observer.Create>(x =>
+ {
+ if (x.HasValue)
+ {
+ o.SetValue(property, x.Value, priority);
+ }
+ }),
+ o.GetBindingObservable(property));
+ }
+
+ ///
+ /// Gets a subject for a .
+ ///
+ /// The property type.
+ /// The object.
+ /// The property.
+ ///
+ /// The priority with which binding values are written to the object.
+ ///
+ ///
+ /// An which can be used for two-way binding to/from the
+ /// property.
+ ///
+ public static ISubject> GetBindingSubject(
+ this IAvaloniaObject o,
+ AvaloniaProperty property,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ return Subject.Create>(
+ Observer.Create>(x =>
+ {
+ if (x.HasValue)
+ {
+ o.SetValue(property, x.Value, priority);
+ }
+ }),
+ o.GetBindingObservable(property));
+ }
+
+ ///
+ /// Binds a to an observable.
+ ///
+ /// The object.
+ /// The property.
+ /// The observable.
+ /// The priority of the binding.
+ ///
+ /// A disposable which can be used to terminate the binding.
+ ///
+ public static IDisposable Bind(
+ this IAvaloniaObject target,
+ AvaloniaProperty property,
+ IObservable> source,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ source = source ?? throw new ArgumentNullException(nameof(source));
+
+ return property.RouteBind(target, source, priority);
+ }
+
+ ///
+ /// Binds a to an observable.
+ ///
+ /// The type of the property.
+ /// The object.
+ /// The property.
+ /// The observable.
+ /// The priority of the binding.
+ ///
+ /// A disposable which can be used to terminate the binding.
+ ///
+ public static IDisposable Bind(
+ this IAvaloniaObject target,
+ AvaloniaProperty property,
+ IObservable> source,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ source = source ?? throw new ArgumentNullException(nameof(source));
+
+ return property switch
+ {
+ StyledPropertyBase styled => target.Bind(styled, source, priority),
+ DirectPropertyBase direct => target.Bind(direct, source),
+ _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
+ };
+ }
+
+ ///
+ /// Binds a to an observable.
+ ///
+ /// The object.
+ /// The property.
+ /// The observable.
+ /// The priority of the binding.
+ ///
+ /// A disposable which can be used to terminate the binding.
+ ///
+ public static IDisposable Bind(
+ this IAvaloniaObject target,
+ AvaloniaProperty property,
+ IObservable source,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ source = source ?? throw new ArgumentNullException(nameof(source));
+
+ return target.Bind(
+ property,
+ source.ToBindingValue(),
+ priority);
+ }
+
+ ///
+ /// Binds a to an observable.
+ ///
+ /// The object.
+ /// The property.
+ /// The observable.
+ /// The priority of the binding.
+ ///
+ /// A disposable which can be used to terminate the binding.
+ ///
+ public static IDisposable Bind(
+ this IAvaloniaObject target,
+ AvaloniaProperty property,
+ IObservable source,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ source = source ?? throw new ArgumentNullException(nameof(source));
+
+ return target.Bind(
+ property,
+ source.ToBindingValue(),
+ priority);
+ }
+
///
/// Binds a property on an to an .
///
@@ -153,16 +359,16 @@ public static IDisposable Bind(
IBinding binding,
object anchor = null)
{
- Contract.Requires(target != null);
- Contract.Requires(property != null);
- Contract.Requires(binding != null);
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+ binding = binding ?? throw new ArgumentNullException(nameof(binding));
var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata;
var result = binding.Initiate(
target,
property,
- anchor,
+ anchor,
metadata?.EnableDataValidation ?? false);
if (result != null)
@@ -175,6 +381,125 @@ public static IDisposable Bind(
}
}
+ ///
+ /// Clears a 's local value.
+ ///
+ /// The object.
+ /// The property.
+ public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
+ property.RouteClearValue(target);
+ }
+
+ ///
+ /// Clears a 's local value.
+ ///
+ /// The object.
+ /// The property.
+ public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
+ switch (property)
+ {
+ case StyledPropertyBase styled:
+ target.ClearValue(styled);
+ break;
+ case DirectPropertyBase direct:
+ target.ClearValue(direct);
+ break;
+ default:
+ throw new NotSupportedException("Unsupported AvaloniaProperty type.");
+ }
+ }
+
+ ///
+ /// Gets a value.
+ ///
+ /// The object.
+ /// The property.
+ /// The value.
+ public static object GetValue(this IAvaloniaObject target, AvaloniaProperty property)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
+ return property.RouteGetValue(target);
+ }
+
+ ///
+ /// Gets a value.
+ ///
+ /// The type of the property.
+ /// The object.
+ /// The property.
+ /// The value.
+ public static T GetValue(this IAvaloniaObject target, AvaloniaProperty property)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
+ return property switch
+ {
+ StyledPropertyBase styled => target.GetValue(styled),
+ DirectPropertyBase direct => target.GetValue(direct),
+ _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
+ };
+ }
+
+ ///
+ /// Sets a value.
+ ///
+ /// The object.
+ /// The property.
+ /// The value.
+ /// The priority of the value.
+ public static void SetValue(
+ this IAvaloniaObject target,
+ AvaloniaProperty property,
+ object value,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
+ property.RouteSetValue(target, value, priority);
+ }
+
+ ///
+ /// Sets a value.
+ ///
+ /// The type of the property.
+ /// The object.
+ /// The property.
+ /// The value.
+ /// The priority of the value.
+ public static void SetValue(
+ this IAvaloniaObject target,
+ AvaloniaProperty property,
+ T value,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ target = target ?? throw new ArgumentNullException(nameof(target));
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
+ switch (property)
+ {
+ case StyledPropertyBase styled:
+ target.SetValue(styled, value, priority);
+ break;
+ case DirectPropertyBase direct:
+ target.SetValue(direct, value);
+ break;
+ default:
+ throw new NotSupportedException("Unsupported AvaloniaProperty type.");
+ }
+ }
+
///
/// Subscribes to a property changed notifications for changes that originate from a
/// .
diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index ac7d2c60afa..e1d4a23441f 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -14,7 +14,7 @@ namespace Avalonia
///
/// Base class for avalonia properties.
///
- public class AvaloniaProperty : IEquatable
+ public abstract class AvaloniaProperty : IEquatable
{
///
/// Represents an unset property value.
@@ -183,6 +183,8 @@ protected AvaloniaProperty(
///
internal int Id { get; }
+ internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false;
+
///
/// Provides access to a property's binding via the
/// indexer.
@@ -255,7 +257,8 @@ protected AvaloniaProperty(
/// The default value of the property.
/// Whether the property inherits its value.
/// The default binding mode for the property.
- /// A validation function.
+ /// A value validation callback.
+ /// A value coercion callback.
///
/// 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 +270,8 @@ public static StyledProperty Register(
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
- Func validate = null,
+ Func validate = null,
+ Func coerce = null,
Action notifying = null)
where TOwner : IAvaloniaObject
{
@@ -275,14 +279,15 @@ public static StyledProperty Register(
var metadata = new StyledPropertyMetadata(
defaultValue,
- validate: Cast(validate),
- defaultBindingMode: defaultBindingMode);
+ defaultBindingMode: defaultBindingMode,
+ coerce: coerce);
var result = new StyledProperty(
name,
typeof(TOwner),
metadata,
inherits,
+ validate,
notifying);
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
@@ -298,24 +303,26 @@ public static StyledProperty Register(
/// The default value of the property.
/// Whether the property inherits its value.
/// The default binding mode for the property.
- /// A validation function.
+ /// A value validation callback.
+ /// A value coercion callback.
/// A
public static AttachedProperty RegisterAttached(
string name,
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
- Func validate = null)
+ Func validate = null,
+ Func coerce = null)
where THost : IAvaloniaObject
{
Contract.Requires(name != null);
var metadata = new StyledPropertyMetadata(
defaultValue,
- validate: Cast(validate),
- defaultBindingMode: defaultBindingMode);
+ defaultBindingMode: defaultBindingMode,
+ coerce: coerce);
- var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits);
+ var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits, validate);
var registry = AvaloniaPropertyRegistry.Instance;
registry.Register(typeof(TOwner), result);
registry.RegisterAttached(typeof(THost), result);
@@ -332,7 +339,8 @@ public static AttachedProperty RegisterAttached(
/// The default value of the property.
/// Whether the property inherits its value.
/// The default binding mode for the property.
- /// A validation function.
+ /// A value validation callback.
+ /// A value coercion callback.
/// A
public static AttachedProperty RegisterAttached(
string name,
@@ -340,17 +348,18 @@ public static AttachedProperty RegisterAttached(
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
- Func validate = null)
+ Func validate = null,
+ Func coerce = null)
where THost : IAvaloniaObject
{
Contract.Requires(name != null);
var metadata = new StyledPropertyMetadata(
defaultValue,
- validate: Cast(validate),
- defaultBindingMode: defaultBindingMode);
+ defaultBindingMode: defaultBindingMode,
+ coerce: coerce);
- var result = new AttachedProperty(name, ownerType, metadata, inherits);
+ var result = new AttachedProperty(name, ownerType, metadata, inherits, validate);
var registry = AvaloniaPropertyRegistry.Instance;
registry.Register(ownerType, result);
registry.RegisterAttached(typeof(THost), result);
@@ -365,9 +374,7 @@ public static AttachedProperty RegisterAttached(
/// The name of the property.
/// Gets the current value of the property.
/// Sets the value of the property.
- ///
- /// The value to use when the property is set to
- ///
+ /// The value to use when the property is cleared.
/// The default binding mode for the property.
///
/// Whether the property is interested in data validation.
@@ -383,13 +390,18 @@ public static DirectProperty RegisterDirect(
where TOwner : IAvaloniaObject
{
Contract.Requires(name != null);
+ Contract.Requires(getter != null);
var metadata = new DirectPropertyMetadata(
unsetValue: unsetValue,
- defaultBindingMode: defaultBindingMode,
- enableDataValidation: enableDataValidation);
+ defaultBindingMode: defaultBindingMode);
- var result = new DirectProperty(name, getter, setter, metadata);
+ var result = new DirectProperty(
+ name,
+ getter,
+ setter,
+ metadata,
+ enableDataValidation);
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}
@@ -483,6 +495,12 @@ public override string ToString()
///
internal bool HasNotifyInitializedObservers => _initialized.HasObservers;
+ ///
+ /// Notifies the observable.
+ ///
+ /// The object being initialized.
+ internal abstract void NotifyInitialized(IAvaloniaObject o);
+
///
/// Notifies the observable.
///
@@ -501,6 +519,42 @@ internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e)
_changed.OnNext(e);
}
+ ///
+ /// Routes an untyped ClearValue call to a typed call.
+ ///
+ /// The object instance.
+ internal abstract void RouteClearValue(IAvaloniaObject o);
+
+ ///
+ /// Routes an untyped GetValue call to a typed call.
+ ///
+ /// The object instance.
+ internal abstract object RouteGetValue(IAvaloniaObject o);
+
+ ///
+ /// Routes an untyped SetValue call to a typed call.
+ ///
+ /// The object instance.
+ /// The value.
+ /// The priority.
+ internal abstract void RouteSetValue(
+ IAvaloniaObject o,
+ object value,
+ BindingPriority priority);
+
+ ///
+ /// Routes an untyped Bind call to a typed call.
+ ///
+ /// The object instance.
+ /// The binding source.
+ /// The priority.
+ internal abstract IDisposable RouteBind(
+ IAvaloniaObject o,
+ IObservable> source,
+ BindingPriority priority);
+
+ internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent);
+
///
/// Overrides the metadata for the property on the specified type.
///
@@ -555,28 +609,15 @@ private PropertyMetadata GetMetadataWithOverrides(Type type)
return _defaultMetadata;
}
-
- [DebuggerHidden]
- private static Func Cast(Func f)
- where TOwner : IAvaloniaObject
- {
- if (f != null)
- {
- return (o, v) => (o is TOwner) ? f((TOwner)o, v) : v;
- }
- else
- {
- return null;
- }
- }
-
-
}
+
///
/// Class representing the .
///
- public class UnsetValueType
+ public sealed class UnsetValueType
{
+ internal UnsetValueType() { }
+
///
/// Returns the string representation of the .
///
diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
index 6082367723f..479d730e48d 100644
--- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
@@ -4,32 +4,20 @@
using System;
using Avalonia.Data;
+#nullable enable
+
namespace Avalonia
{
///
/// Provides information for a avalonia property change.
///
- public class AvaloniaPropertyChangedEventArgs : EventArgs
+ public abstract class AvaloniaPropertyChangedEventArgs : EventArgs
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The object that the property changed on.
- /// The property that changed.
- /// The old value of the property.
- /// The new value of the property.
- /// The priority of the binding that produced the value.
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 @@ public AvaloniaPropertyChangedEventArgs(
/// Gets the that the property changed on.
///
/// The sender object.
- public AvaloniaObject Sender { get; private set; }
+ public IAvaloniaObject Sender { get; }
///
/// Gets the property that changed.
@@ -45,30 +33,36 @@ public AvaloniaPropertyChangedEventArgs(
///
/// The property that changed.
///
- public AvaloniaProperty Property { get; private set; }
+ public AvaloniaProperty Property => GetProperty();
///
/// Gets the old value of the property.
///
///
- /// The old value of the property.
+ /// The old value of the property or if the
+ /// property previously had no value.
///
- public object OldValue { get; private set; }
+ public object? OldValue => GetOldValue();
///
/// Gets the new value of the property.
///
///
- /// The new value of the property.
+ /// The new value of the property or if the
+ /// property previously had no value.
///
- public object NewValue { get; private set; }
+ public object? NewValue => GetNewValue();
///
/// Gets the priority of the binding that produced the value.
///
///
- /// The priority of the binding that produced the value.
+ /// The priority of the new value.
///
public BindingPriority Priority { get; private set; }
+
+ protected abstract AvaloniaProperty GetProperty();
+ protected abstract object? GetOldValue();
+ protected abstract object? GetNewValue();
}
}
diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
new file mode 100644
index 00000000000..d8ac3752b34
--- /dev/null
+++ b/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
+{
+ ///
+ /// Provides information for a avalonia property change.
+ ///
+ public class AvaloniaPropertyChangedEventArgs : AvaloniaPropertyChangedEventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that the property changed on.
+ /// The property that changed.
+ /// The old value of the property.
+ /// The new value of the property.
+ /// The priority of the binding that produced the value.
+ public AvaloniaPropertyChangedEventArgs(
+ IAvaloniaObject sender,
+ AvaloniaProperty property,
+ Optional oldValue,
+ BindingValue newValue,
+ BindingPriority priority)
+ : base(sender, priority)
+ {
+ Property = property;
+ OldValue = oldValue;
+ NewValue = newValue;
+ }
+
+ ///
+ /// Gets the property that changed.
+ ///
+ ///
+ /// The property that changed.
+ ///
+ public new AvaloniaProperty Property { get; }
+
+ ///
+ /// Gets the old value of the property.
+ ///
+ ///
+ /// The old value of the property.
+ ///
+ public new Optional OldValue { get; private set; }
+
+ ///
+ /// Gets the new value of the property.
+ ///
+ ///
+ /// The new value of the property.
+ ///
+ public new BindingValue NewValue { get; private set; }
+
+ protected override AvaloniaProperty GetProperty() => Property;
+
+ protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);
+
+ protected override object? GetNewValue() => NewValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);
+ }
+}
diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
index 01daeafc3a7..14c86305990 100644
--- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
@@ -20,10 +20,14 @@ public class AvaloniaPropertyRegistry
new Dictionary>();
private readonly Dictionary> _attached =
new Dictionary>();
+ private readonly Dictionary> _direct =
+ new Dictionary>();
private readonly Dictionary> _registeredCache =
new Dictionary>();
private readonly Dictionary> _attachedCache =
new Dictionary>();
+ private readonly Dictionary> _directCache =
+ new Dictionary>();
private readonly Dictionary> _initializedCache =
new Dictionary>();
private readonly Dictionary> _inheritedCache =
@@ -105,6 +109,37 @@ public IEnumerable GetRegisteredAttached(Type type)
return result;
}
+ ///
+ /// Gets all direct s registered on a type.
+ ///
+ /// The type.
+ /// A collection of definitions.
+ public IEnumerable GetRegisteredDirect(Type type)
+ {
+ Contract.Requires(type != null);
+
+ if (_directCache.TryGetValue(type, out var result))
+ {
+ return result;
+ }
+
+ var t = type;
+ result = new List();
+
+ while (t != null)
+ {
+ if (_direct.TryGetValue(t, out var direct))
+ {
+ result.AddRange(direct.Values);
+ }
+
+ t = t.BaseType;
+ }
+
+ _directCache.Add(type, result);
+ return result;
+ }
+
///
/// Gets all inherited s registered on a type.
///
@@ -150,13 +185,29 @@ public IEnumerable GetRegisteredInherited(Type type)
///
/// The object.
/// A collection of definitions.
- public IEnumerable GetRegistered(AvaloniaObject o)
+ public IEnumerable GetRegistered(IAvaloniaObject o)
{
Contract.Requires(o != null);
return GetRegistered(o.GetType());
}
+ ///
+ /// Finds a direct property as registered on an object.
+ ///
+ /// The object.
+ /// The direct property.
+ ///
+ /// The registered property or null if no matching property found.
+ ///
+ public DirectPropertyBase GetRegisteredDirect(
+ IAvaloniaObject o,
+ DirectPropertyBase property)
+ {
+ return FindRegisteredDirect(o, property) ??
+ throw new ArgumentException($"Property '{property.Name} not registered on '{o.GetType()}");
+ }
+
///
/// Finds a registered property on a type by name.
///
@@ -200,7 +251,7 @@ public AvaloniaProperty FindRegistered(Type type, string name)
///
/// The property name contains a '.'.
///
- public AvaloniaProperty FindRegistered(AvaloniaObject o, string name)
+ public AvaloniaProperty FindRegistered(IAvaloniaObject o, string name)
{
Contract.Requires(o != null);
Contract.Requires(name != null);
@@ -208,6 +259,34 @@ public AvaloniaProperty FindRegistered(AvaloniaObject o, string name)
return FindRegistered(o.GetType(), name);
}
+ ///
+ /// Finds a direct property as registered on an object.
+ ///
+ /// The object.
+ /// The direct property.
+ ///
+ /// The registered property or null if no matching property found.
+ ///
+ public DirectPropertyBase FindRegisteredDirect(
+ IAvaloniaObject o,
+ DirectPropertyBase property)
+ {
+ if (property.Owner == o.GetType())
+ {
+ return property;
+ }
+
+ foreach (var p in GetRegisteredDirect(o.GetType()))
+ {
+ if (p == property)
+ {
+ return (DirectPropertyBase)p;
+ }
+ }
+
+ return null;
+ }
+
///
/// Finds a registered property by Id.
///
@@ -273,6 +352,22 @@ public void Register(Type type, AvaloniaProperty property)
inner.Add(property.Id, property);
}
+ if (property.IsDirect)
+ {
+ if (!_direct.TryGetValue(type, out inner))
+ {
+ inner = new Dictionary();
+ 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);
@@ -326,18 +421,6 @@ internal void NotifyInitialized(AvaloniaObject o)
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();
@@ -373,14 +456,7 @@ void Notify(AvaloniaProperty property, object value)
foreach (PropertyInitializationData data in initializationData)
{
- if (!data.Property.HasNotifyInitializedObservers)
- {
- continue;
- }
-
- object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value;
-
- Notify(data.Property, value);
+ data.Property.NotifyInitialized(o);
}
}
diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs
index 0a223cf7ee0..be58ff796d5 100644
--- a/src/Avalonia.Base/AvaloniaProperty`1.cs
+++ b/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.
///
/// The value type of the property.
- public class AvaloniaProperty : AvaloniaProperty
+ public abstract class AvaloniaProperty : AvaloniaProperty
{
///
/// Initializes a new instance of the class.
@@ -40,5 +42,29 @@ protected AvaloniaProperty(
: base(source, ownerType, metadata)
{
}
+
+ protected BindingValue TryConvert(object value)
+ {
+ if (value == UnsetValue)
+ {
+ return BindingValue.Unset;
+ }
+ else if (value == BindingOperations.DoNothing)
+ {
+ return BindingValue.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.BindingError(error);
+ }
+
+ return converted;
+ }
}
}
diff --git a/src/Avalonia.Base/BoxedValue.cs b/src/Avalonia.Base/BoxedValue.cs
deleted file mode 100644
index 5fc515f2996..00000000000
--- a/src/Avalonia.Base/BoxedValue.cs
+++ /dev/null
@@ -1,28 +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.
-
-namespace Avalonia
-{
- ///
- /// Represents boxed value of type .
- ///
- /// Type of stored value.
- internal readonly struct BoxedValue
- {
- public BoxedValue(T value)
- {
- Boxed = value;
- Typed = value;
- }
-
- ///
- /// Boxed value.
- ///
- public object Boxed { get; }
-
- ///
- /// Typed value.
- ///
- public T Typed { get; }
- }
-}
diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs
index 7c55321a805..9a2cc1bfdee 100644
--- a/src/Avalonia.Base/Data/BindingNotification.cs
+++ b/src/Avalonia.Base/Data/BindingNotification.cs
@@ -30,6 +30,12 @@ public enum BindingErrorType
/// Represents a binding notification that can be a valid binding value, or a binding or
/// data validation error.
///
+ ///
+ /// This class is very similar to , but where
+ /// is used by typed bindings, this class is used to hold binding and data validation errors in
+ /// untyped bindings. As Avalonia moves towards using typed bindings by default we may want to remove
+ /// this class.
+ ///
public class BindingNotification
{
///
@@ -236,6 +242,26 @@ public void SetValue(object value)
_value = value;
}
+ public BindingValue ToBindingValue()
+ {
+ if (ErrorType == BindingErrorType.None)
+ {
+ return HasValue ? new BindingValue(Value) : BindingValue.Unset;
+ }
+ else if (ErrorType == BindingErrorType.Error)
+ {
+ return BindingValue.BindingError(
+ Error,
+ HasValue ? new Optional(Value) : Optional.Empty);
+ }
+ else
+ {
+ return BindingValue.DataValidationError(
+ Error,
+ HasValue ? new Optional(Value) : Optional.Empty);
+ }
+ }
+
///
public override string ToString()
{
diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs
index 256de2f902f..1b47cc7490e 100644
--- a/src/Avalonia.Base/Data/BindingOperations.cs
+++ b/src/Avalonia.Base/Data/BindingOperations.cs
@@ -4,12 +4,13 @@
using System;
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();
///
/// Applies an a property on an .
@@ -63,7 +64,10 @@ public static IDisposable Apply(
return source
.Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
.Take(1)
- .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority));
+ .Subscribe(x => targetCopy.SetValue(
+ propertyCopy,
+ BindingNotification.ExtractValue(x),
+ bindingCopy.Priority));
}
else
{
@@ -88,4 +92,15 @@ public static IDisposable Apply(
}
}
}
+
+ public sealed class DoNothingType
+ {
+ internal DoNothingType() { }
+
+ ///
+ /// Returns the string representation of .
+ ///
+ /// The string "(do nothing)".
+ public override string ToString() => "(do nothing)";
+ }
}
diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs
new file mode 100644
index 00000000000..cecdd33e7b4
--- /dev/null
+++ b/src/Avalonia.Base/Data/BindingValue.cs
@@ -0,0 +1,432 @@
+using System;
+using Avalonia.Utilities;
+
+#nullable enable
+
+namespace Avalonia.Data
+{
+ ///
+ /// Describes the type of a .
+ ///
+ [Flags]
+ public enum BindingValueType
+ {
+ ///
+ /// An unset value: the target property will revert to its unbound state until a new
+ /// binding value is produced.
+ ///
+ UnsetValue = 0,
+
+ ///
+ /// Do nothing: the binding value will be ignored.
+ ///
+ DoNothing = 1,
+
+ ///
+ /// A simple value.
+ ///
+ Value = 2 | HasValue,
+
+ ///
+ /// A binding error, such as a missing source property.
+ ///
+ BindingError = 3 | HasError,
+
+ ///
+ /// A data validation error.
+ ///
+ DataValidationError = 4 | HasError,
+
+ ///
+ /// A binding error with a fallback value.
+ ///
+ BindingErrorWithFallback = BindingError | HasValue,
+
+ ///
+ /// A data validation error with a fallback value.
+ ///
+ DataValidationErrorWithFallback = DataValidationError | HasValue,
+
+ TypeMask = 0x00ff,
+ HasValue = 0x0100,
+ HasError = 0x0200,
+ }
+
+ ///
+ /// A value passed into a binding.
+ ///
+ /// The value type.
+ ///
+ /// 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
+ /// property:
+ ///
+ /// - : a simple value
+ /// - : the target property will revert to its unbound
+ /// state until a new binding value is produced. Represented by
+ /// in an untyped context
+ /// - : the binding value will be ignored. Represented
+ /// by in an untyped context
+ /// - : a binding error, such as a missing source
+ /// property, with an optional fallback value
+ /// - : a data validation error, with an
+ /// optional fallback value
+ ///
+ /// To create a new binding value you can:
+ ///
+ /// - For a simple value, call the constructor or use an implicit
+ /// conversion from
+ /// - For an unset value, use or simply `default`
+ /// - For other types, call one of the static factory methods
+ ///
+ public readonly struct BindingValue
+ {
+ private readonly T _value;
+
+ ///
+ /// Initializes a new instance of the struct with a type of
+ ///
+ ///
+ /// The value.
+ 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;
+ }
+
+ ///
+ /// Gets a value indicating whether the binding value represents either a binding or data
+ /// validation error.
+ ///
+ public bool HasError => Type.HasFlagCustom(BindingValueType.HasError);
+
+ ///
+ /// Gets a value indicating whether the binding value has a value.
+ ///
+ public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue);
+
+ ///
+ /// Gets the type of the binding value.
+ ///
+ public BindingValueType Type { get; }
+
+ ///
+ /// Gets the binding value or fallback value.
+ ///
+ ///
+ /// is false.
+ ///
+ public T Value => HasValue ? _value : throw new InvalidOperationException("BindingValue has no value.");
+
+ ///
+ /// Gets the binding or data validation error.
+ ///
+ public Exception? Error { get; }
+
+ ///
+ /// Converts the binding value to an .
+ ///
+ ///
+ public Optional ToOptional() => HasValue ? new Optional(_value) : default;
+
+ ///
+ public override string ToString() => HasError ? $"Error: {Error!.Message}" : _value?.ToString() ?? "(null)";
+
+ ///
+ /// Converts the value to untyped representation, using ,
+ /// and where
+ /// appropriate.
+ ///
+ /// The untyped representation of the binding value.
+ 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("Invalid BindingValueType."),
+ };
+ }
+
+ ///
+ /// Returns a new binding value with the specified value.
+ ///
+ /// The new value.
+ /// The new binding value.
+ ///
+ /// The binding type is or
+ /// .
+ ///
+ public BindingValue 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(type | BindingValueType.HasValue, value, Error);
+ }
+
+ ///
+ /// Gets the value of the binding value if present, otherwise the default value.
+ ///
+ /// The value.
+ public T GetValueOrDefault() => HasValue ? _value : default;
+
+ ///
+ /// Gets the value of the binding value if present, otherwise a default value.
+ ///
+ /// The default value.
+ /// The value.
+ public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue;
+
+ ///
+ /// Gets the value if present, otherwise the default value.
+ ///
+ ///
+ /// The value if present and of the correct type, `default(TResult)` if the value is
+ /// not present or of an incorrect type.
+ ///
+ public TResult GetValueOrDefault()
+ {
+ return HasValue ?
+ _value is TResult result ? result : default
+ : default;
+ }
+
+ ///
+ /// Gets the value of the binding value if present, otherwise a default value.
+ ///
+ /// The default value.
+ ///
+ /// 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 if the
+ /// value is not present.
+ ///
+ public TResult GetValueOrDefault(TResult defaultValue)
+ {
+ return HasValue ?
+ _value is TResult result ? result : default
+ : defaultValue;
+ }
+
+ ///
+ /// Creates a from an object, handling the special values
+ /// and .
+ ///
+ /// The untyped value.
+ /// The typed binding value.
+ public static BindingValue FromUntyped(object? value)
+ {
+ return value switch
+ {
+ UnsetValueType _ => Unset,
+ DoNothingType _ => DoNothing,
+ BindingNotification n => n.ToBindingValue().Cast(),
+ _ => (T)value
+ };
+ }
+
+ ///
+ /// Creates a binding value from an instance of the underlying value type.
+ ///
+ /// The value.
+ public static implicit operator BindingValue(T value) => new BindingValue(value);
+
+ ///
+ /// Creates a binding value from an .
+ ///
+ /// The optional value.
+
+ public static implicit operator BindingValue(Optional optional)
+ {
+ return optional.HasValue ? optional.Value : Unset;
+ }
+
+ ///
+ /// Returns a binding value with a type of .
+ ///
+ public static BindingValue Unset => new BindingValue(BindingValueType.UnsetValue, default, null);
+
+ ///
+ /// Returns a binding value with a type of .
+ ///
+ public static BindingValue DoNothing => new BindingValue(BindingValueType.DoNothing, default, null);
+
+ ///
+ /// Returns a binding value with a type of .
+ ///
+ /// The binding error.
+ public static BindingValue BindingError(Exception e)
+ {
+ e = e ?? throw new ArgumentNullException("e");
+
+ return new BindingValue(BindingValueType.BindingError, default, e);
+ }
+
+ ///
+ /// Returns a binding value with a type of .
+ ///
+ /// The binding error.
+ /// The fallback value.
+ public static BindingValue BindingError(Exception e, T fallbackValue)
+ {
+ e = e ?? throw new ArgumentNullException("e");
+
+ return new BindingValue(BindingValueType.BindingErrorWithFallback, fallbackValue, e);
+ }
+
+ ///
+ /// Returns a binding value with a type of or
+ /// .
+ ///
+ /// The binding error.
+ /// The fallback value.
+ public static BindingValue BindingError(Exception e, Optional fallbackValue)
+ {
+ e = e ?? throw new ArgumentNullException("e");
+
+ return new BindingValue(
+ fallbackValue.HasValue ?
+ BindingValueType.BindingErrorWithFallback :
+ BindingValueType.BindingError,
+ fallbackValue.HasValue ? fallbackValue.Value : default,
+ e);
+ }
+
+ ///
+ /// Returns a binding value with a type of .
+ ///
+ /// The data validation error.
+ public static BindingValue DataValidationError(Exception e)
+ {
+ e = e ?? throw new ArgumentNullException("e");
+
+ return new BindingValue(BindingValueType.DataValidationError, default, e);
+ }
+
+ ///
+ /// Returns a binding value with a type of .
+ ///
+ /// The data validation error.
+ /// The fallback value.
+ public static BindingValue DataValidationError(Exception e, T fallbackValue)
+ {
+ e = e ?? throw new ArgumentNullException("e");
+
+ return new BindingValue(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e);
+ }
+
+ ///
+ /// Returns a binding value with a type of or
+ /// .
+ ///
+ /// The binding error.
+ /// The fallback value.
+ public static BindingValue DataValidationError(Exception e, Optional fallbackValue)
+ {
+ e = e ?? throw new ArgumentNullException("e");
+
+ return new BindingValue(
+ 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<>.");
+ }
+
+ if (value is BindingValue)
+ {
+ throw new InvalidOperationException("BindingValue cannot be wrapped in a BindingValue<>.");
+ }
+ }
+ }
+
+ public static class BindingValueExtensions
+ {
+ ///
+ /// Casts the type of a using only the C# cast operator.
+ ///
+ /// The target type.
+ /// The binding value.
+ /// The cast value.
+ public static BindingValue Cast(this BindingValue value)
+ {
+ return value.Type switch
+ {
+ BindingValueType.DoNothing => BindingValue.DoNothing,
+ BindingValueType.UnsetValue => BindingValue.Unset,
+ BindingValueType.Value => new BindingValue((T)value.Value),
+ BindingValueType.BindingError => BindingValue.BindingError(value.Error!),
+ BindingValueType.BindingErrorWithFallback => BindingValue.BindingError(
+ value.Error!,
+ (T)value.Value),
+ BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!),
+ BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError(
+ value.Error!,
+ (T)value.Value),
+ _ => throw new NotSupportedException("Invalid BindingValue type."),
+ };
+ }
+
+ ///
+ /// Casts the type of a using the implicit conversions
+ /// allowed by the C# language.
+ ///
+ /// The target type.
+ /// The binding value.
+ /// The cast value.
+ ///
+ /// Note that this method uses reflection and as such may be slow.
+ ///
+ public static BindingValue Convert(this BindingValue value)
+ {
+ return value.Type switch
+ {
+ BindingValueType.DoNothing => BindingValue.DoNothing,
+ BindingValueType.UnsetValue => BindingValue.Unset,
+ BindingValueType.Value => new BindingValue(TypeUtilities.ConvertImplicit(value.Value)),
+ BindingValueType.BindingError => BindingValue.BindingError(value.Error!),
+ BindingValueType.BindingErrorWithFallback => BindingValue.BindingError(
+ value.Error!,
+ TypeUtilities.ConvertImplicit(value.Value)),
+ BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!),
+ BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError(
+ value.Error!,
+ TypeUtilities.ConvertImplicit(value.Value)),
+ _ => throw new NotSupportedException("Invalid BindingValue type."),
+ };
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs
new file mode 100644
index 00000000000..dd952c895c7
--- /dev/null
+++ b/src/Avalonia.Base/Data/Optional.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+
+#nullable enable
+
+namespace Avalonia.Data
+{
+ ///
+ /// An optional typed value.
+ ///
+ /// The value type.
+ ///
+ /// This struct is similar to except it also accepts reference types:
+ /// note that null is a valid value for reference types. It is also similar to
+ /// but has only two states: "value present" and "value missing".
+ ///
+ /// To create a new optional value you can:
+ ///
+ /// - For a simple value, call the constructor or use an implicit
+ /// conversion from
+ /// - For an missing value, use or simply `default`
+ ///
+ public readonly struct Optional : IEquatable>
+ {
+ private readonly T _value;
+
+ ///
+ /// Initializes a new instance of the struct with value.
+ ///
+ /// The value.
+ public Optional(T value)
+ {
+ _value = value;
+ HasValue = true;
+ }
+
+ ///
+ /// Gets a value indicating whether a value is present.
+ ///
+ public bool HasValue { get; }
+
+ ///
+ /// Gets the value.
+ ///
+ ///
+ /// is false.
+ ///
+ public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value.");
+
+ ///
+ public override bool Equals(object obj) => obj is Optional o && this == o;
+
+ ///
+ public bool Equals(Optional other) => this == other;
+
+ ///
+ public override int GetHashCode() => HasValue ? _value?.GetHashCode() ?? 0 : 0;
+
+ ///
+ /// Casts the value (if any) to an .
+ ///
+ /// The cast optional value.
+ public Optional ToObject() => HasValue ? new Optional(_value) : default;
+
+ ///
+ public override string ToString() => HasValue ? _value?.ToString() ?? "(null)" : "(empty)";
+
+ ///
+ /// Gets the value if present, otherwise the default value.
+ ///
+ /// The value.
+ public T GetValueOrDefault() => HasValue ? _value : default;
+
+ ///
+ /// Gets the value if present, otherwise a default value.
+ ///
+ /// The default value.
+ /// The value.
+ public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue;
+
+ ///
+ /// Gets the value if present, otherwise the default value.
+ ///
+ ///
+ /// The value if present and of the correct type, `default(TResult)` if the value is
+ /// not present or of an incorrect type.
+ ///
+ public TResult GetValueOrDefault()
+ {
+ return HasValue ?
+ _value is TResult result ? result : default
+ : default;
+ }
+
+ ///
+ /// Gets the value if present, otherwise a default value.
+ ///
+ /// The default value.
+ ///
+ /// 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 if the
+ /// value is not present.
+ ///
+ public TResult GetValueOrDefault(TResult defaultValue)
+ {
+ return HasValue ?
+ _value is TResult result ? result : default
+ : defaultValue;
+ }
+
+ ///
+ /// Creates an from an instance of the underlying value type.
+ ///
+ /// The value.
+ public static implicit operator Optional(T value) => new Optional(value);
+
+ ///
+ /// Compares two s for inequality.
+ ///
+ /// The first value.
+ /// The second value.
+ /// True if the values are unequal; otherwise false.
+ public static bool operator !=(Optional x, Optional y) => !(x == y);
+
+ ///
+ /// Compares two s for equality.
+ ///
+ /// The first value.
+ /// The second value.
+ /// True if the values are equal; otherwise false.
+ public static bool operator==(Optional x, Optional y)
+ {
+ if (!x.HasValue && !y.HasValue)
+ {
+ return true;
+ }
+ else if (x.HasValue && y.HasValue)
+ {
+ return EqualityComparer.Default.Equals(x.Value, y.Value);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Returns an without a value.
+ ///
+ public static Optional Empty => default;
+ }
+}
diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
index 7afbcabd2ad..d062856a739 100644
--- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
+++ b/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,7 @@ public static class AvaloniaObjectExtensions
///
public static AvaloniaPropertyValue GetDiagnostic(this AvaloniaObject o, AvaloniaProperty property)
{
- 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");
- }
+ return o.GetDiagnosticInternal(property);
}
}
}
diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs
index 1ce73c20ba2..2a8c7316141 100644
--- a/src/Avalonia.Base/DirectProperty.cs
+++ b/src/Avalonia.Base/DirectProperty.cs
@@ -16,7 +16,7 @@ namespace Avalonia
/// system. They hold a getter and an optional setter which
/// allows the avalonia property system to read and write the current value.
///
- public class DirectProperty : AvaloniaProperty, IDirectPropertyAccessor
+ public class DirectProperty : DirectPropertyBase, IDirectPropertyAccessor
where TOwner : IAvaloniaObject
{
///
@@ -26,12 +26,16 @@ public class DirectProperty : AvaloniaProperty, IDirectP
/// Gets the current value of the property.
/// Sets the value of the property. May be null.
/// The property metadata.
+ ///
+ /// Whether the property is interested in data validation.
+ ///
public DirectProperty(
string name,
Func getter,
Action setter,
- DirectPropertyMetadata metadata)
- : base(name, typeof(TOwner), metadata)
+ DirectPropertyMetadata metadata,
+ bool enableDataValidation)
+ : base(name, typeof(TOwner), metadata, enableDataValidation)
{
Contract.Requires(getter != null);
@@ -46,12 +50,16 @@ public DirectProperty(
/// Gets the current value of the property.
/// Sets the value of the property. May be null.
/// Optional overridden metadata.
+ ///
+ /// Whether the property is interested in data validation.
+ ///
private DirectProperty(
- AvaloniaProperty source,
+ DirectPropertyBase source,
Func getter,
Action setter,
- DirectPropertyMetadata metadata)
- : base(source, typeof(TOwner), metadata)
+ DirectPropertyMetadata metadata,
+ bool enableDataValidation)
+ : base(source, typeof(TOwner), metadata, enableDataValidation)
{
Contract.Requires(getter != null);
@@ -65,6 +73,9 @@ private DirectProperty(
///
public override bool IsReadOnly => Setter == null;
+ ///
+ public override Type Owner => typeof(TOwner);
+
///
/// Gets the getter function.
///
@@ -75,9 +86,6 @@ private DirectProperty(
///
public Action Setter { get; }
- ///
- Type IDirectPropertyAccessor.Owner => typeof(TOwner);
-
///
/// Registers the direct property on another type.
///
@@ -99,6 +107,45 @@ public DirectProperty AddOwner(
BindingMode defaultBindingMode = BindingMode.Default,
bool enableDataValidation = false)
where TNewOwner : AvaloniaObject
+ {
+ var metadata = new DirectPropertyMetadata(
+ unsetValue: unsetValue,
+ defaultBindingMode: defaultBindingMode);
+
+ metadata.Merge(GetMetadata(), this);
+
+ var result = new DirectProperty(
+ (DirectPropertyBase)this,
+ getter,
+ setter,
+ metadata,
+ enableDataValidation);
+
+ AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
+ return result;
+ }
+
+ ///
+ /// Registers the direct property on another type.
+ ///
+ /// The type of the additional owner.
+ /// Gets the current value of the property.
+ /// Sets the value of the property.
+ ///
+ /// The value to use when the property is set to
+ ///
+ /// The default binding mode for the property.
+ ///
+ /// Whether the property is interested in data validation.
+ ///
+ /// The property.
+ public DirectProperty AddOwnerWithDataValidation(
+ Func getter,
+ Action setter,
+ TValue unsetValue = default(TValue),
+ BindingMode defaultBindingMode = BindingMode.Default,
+ bool enableDataValidation = false)
+ where TNewOwner : AvaloniaObject
{
var metadata = new DirectPropertyMetadata(
unsetValue: unsetValue,
@@ -111,12 +158,33 @@ public DirectProperty AddOwner(
this,
getter,
setter,
- metadata);
+ metadata,
+ enableDataValidation);
AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return result;
}
+ ///
+ internal override TValue InvokeGetter(IAvaloniaObject instance)
+ {
+ return Getter((TOwner)instance);
+ }
+
+ ///
+ internal override void InvokeSetter(IAvaloniaObject instance, BindingValue value)
+ {
+ if (Setter == null)
+ {
+ throw new ArgumentException($"The property {Name} is readonly.");
+ }
+
+ if (value.HasValue)
+ {
+ Setter((TOwner)instance, value.Value);
+ }
+ }
+
///
object IDirectPropertyAccessor.GetValue(IAvaloniaObject instance)
{
diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs
new file mode 100644
index 00000000000..7a0be065eb6
--- /dev/null
+++ b/src/Avalonia.Base/DirectPropertyBase.cs
@@ -0,0 +1,168 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia
+{
+ ///
+ /// Base class for direct properties.
+ ///
+ /// The type of the property's value.
+ ///
+ /// Whereas is typed on the owner type, this base
+ /// class provides a non-owner-typed interface to a direct poperty.
+ ///
+ public abstract class DirectPropertyBase : AvaloniaProperty
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the property.
+ /// The type of the class that registers the property.
+ /// The property metadata.
+ ///
+ /// Whether the property is interested in data validation.
+ ///
+ protected DirectPropertyBase(
+ string name,
+ Type ownerType,
+ PropertyMetadata metadata,
+ bool enableDataValidation)
+ : base(name, ownerType, metadata)
+ {
+ IsDataValidationEnabled = enableDataValidation;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The property to copy.
+ /// The new owner type.
+ /// Optional overridden metadata.
+ ///
+ /// Whether the property is interested in data validation.
+ ///
+ protected DirectPropertyBase(
+ AvaloniaProperty source,
+ Type ownerType,
+ PropertyMetadata metadata,
+ bool enableDataValidation)
+ : base(source, ownerType, metadata)
+ {
+ IsDataValidationEnabled = enableDataValidation;
+ }
+
+ ///
+ /// Gets the type that registered the property.
+ ///
+ public abstract Type Owner { get; }
+
+ ///
+ /// Gets a value that indicates whether data validation is enabled for the property.
+ ///
+ public bool IsDataValidationEnabled { get; }
+
+ ///
+ /// Gets the value of the property on the instance.
+ ///
+ /// The instance.
+ /// The property value.
+ internal abstract TValue InvokeGetter(IAvaloniaObject instance);
+
+ ///
+ /// Sets the value of the property on the instance.
+ ///
+ /// The instance.
+ /// The value.
+ internal abstract void InvokeSetter(IAvaloniaObject instance, BindingValue value);
+
+ ///
+ /// Gets the unset value for the property on the specified type.
+ ///
+ /// The type.
+ /// The unset value.
+ public TValue GetUnsetValue(Type type)
+ {
+ type = type ?? throw new ArgumentNullException(nameof(type));
+ return GetMetadata(type).UnsetValue;
+ }
+
+ ///
+ /// Gets the property metadata for the specified type.
+ ///
+ /// The type.
+ ///
+ /// The property metadata.
+ ///
+ public new DirectPropertyMetadata GetMetadata(Type type)
+ {
+ return (DirectPropertyMetadata)base.GetMetadata(type);
+ }
+
+ ///
+ internal override void NotifyInitialized(IAvaloniaObject o)
+ {
+ if (HasNotifyInitializedObservers)
+ {
+ var e = new AvaloniaPropertyChangedEventArgs(
+ o,
+ this,
+ default,
+ InvokeGetter(o),
+ BindingPriority.Unset);
+ NotifyInitialized(e);
+ }
+ }
+
+ ///
+ internal override void RouteClearValue(IAvaloniaObject o)
+ {
+ o.ClearValue(this);
+ }
+
+ ///
+ internal override object? RouteGetValue(IAvaloniaObject o)
+ {
+ return o.GetValue(this);
+ }
+
+ ///
+ internal override void RouteSetValue(
+ IAvaloniaObject o,
+ object value,
+ BindingPriority priority)
+ {
+ var v = TryConvert(value);
+
+ if (v.HasValue)
+ {
+ o.SetValue(this, (TValue)v.Value);
+ }
+ else if (v.Type == BindingValueType.UnsetValue)
+ {
+ o.ClearValue(this);
+ }
+ else if (v.HasError)
+ {
+ throw v.Error!;
+ }
+ }
+
+ ///
+ internal override IDisposable RouteBind(
+ IAvaloniaObject o,
+ IObservable> source,
+ BindingPriority priority)
+ {
+ var adapter = TypedBindingAdapter.Create(o, this, source);
+ return o.Bind(this, adapter);
+ }
+
+ internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent)
+ {
+ throw new NotSupportedException("Direct properties do not support inheritance.");
+ }
+ }
+}
diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs
index 5a3829167af..fb85ae222cd 100644
--- a/src/Avalonia.Base/IAvaloniaObject.cs
+++ b/src/Avalonia.Base/IAvaloniaObject.cs
@@ -17,16 +17,24 @@ public interface IAvaloniaObject
event EventHandler PropertyChanged;
///
- /// Raised when an inheritable value changes on this object.
+ /// Clears an 's local value.
///
- event EventHandler InheritablePropertyChanged;
+ /// The property.
+ void ClearValue(StyledPropertyBase property);
+
+ ///
+ /// Clears an 's local value.
+ ///
+ /// The property.
+ void ClearValue(DirectPropertyBase property);
///
/// Gets a value.
///
+ /// The type of the property.
/// The property.
/// The value.
- object GetValue(AvaloniaProperty property);
+ T GetValue(StyledPropertyBase property);
///
/// Gets a value.
@@ -34,7 +42,7 @@ public interface IAvaloniaObject
/// The type of the property.
/// The property.
/// The value.
- T GetValue(AvaloniaProperty property);
+ T GetValue(DirectPropertyBase property);
///
/// Checks whether a is animating.
@@ -53,12 +61,13 @@ public interface IAvaloniaObject
///
/// Sets a value.
///
+ /// The type of the property.
/// The property.
/// The value.
/// The priority of the value.
- void SetValue(
- AvaloniaProperty property,
- object value,
+ void SetValue(
+ StyledPropertyBase property,
+ T value,
BindingPriority priority = BindingPriority.LocalValue);
///
@@ -67,24 +76,21 @@ void SetValue(
/// The type of the property.
/// The property.
/// The value.
- /// The priority of the value.
- void SetValue(
- AvaloniaProperty property,
- T value,
- BindingPriority priority = BindingPriority.LocalValue);
+ void SetValue(DirectPropertyBase property, T value);
///
/// Binds a to an observable.
///
+ /// The type of the property.
/// The property.
/// The observable.
/// The priority of the binding.
///
/// A disposable which can be used to terminate the binding.
///
- IDisposable Bind(
- AvaloniaProperty property,
- IObservable source,
+ IDisposable Bind(
+ StyledPropertyBase property,
+ IObservable> source,
BindingPriority priority = BindingPriority.LocalValue);
///
@@ -93,13 +99,52 @@ IDisposable Bind(
/// The type of the property.
/// The property.
/// The observable.
- /// The priority of the binding.
///
/// A disposable which can be used to terminate the binding.
///
IDisposable Bind(
+ DirectPropertyBase property,
+ IObservable> source);
+
+ ///
+ /// Coerces the specified .
+ ///
+ /// The type of the property.
+ /// The property.
+ void CoerceValue(StyledPropertyBase property);
+
+ ///
+ /// Registers an object as an inheritance child.
+ ///
+ /// The inheritance child.
+ ///
+ /// Inheritance children will receive a call to
+ ///
+ /// when an inheritable property value changes on the parent.
+ ///
+ void AddInheritanceChild(IAvaloniaObject child);
+
+ ///
+ /// Unregisters an object as an inheritance child.
+ ///
+ /// The inheritance child.
+ ///
+ /// Removes an inheritance child that was added by a call to
+ /// .
+ ///
+ void RemoveInheritanceChild(IAvaloniaObject child);
+
+ ///
+ /// Called when an inheritable property changes on an object registered as an inheritance
+ /// parent.
+ ///
+ /// The type of the value.
+ /// The property that has changed.
+ ///
+ ///
+ void InheritedPropertyChanged(
AvaloniaProperty property,
- IObservable source,
- BindingPriority priority = BindingPriority.LocalValue);
+ Optional oldValue,
+ Optional newValue);
}
}
diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs
deleted file mode 100644
index 1d6e5e59ad0..00000000000
--- a/src/Avalonia.Base/IPriorityValueOwner.cs
+++ /dev/null
@@ -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
-{
- ///
- /// An owner of a .
- ///
- internal interface IPriorityValueOwner
- {
- ///
- /// Called when a 's value changes.
- ///
- /// The the property that has changed.
- /// The priority of the value.
- /// The old value.
- /// The new value.
- void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue);
-
- ///
- /// Called when a is received by a
- /// .
- ///
- /// The the property that has changed.
- /// The notification.
- void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
-
- ///
- /// Returns deferred setter for given non-direct property.
- ///
- /// Property.
- /// Deferred setter for given property.
- DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property);
-
- ///
- /// Logs a binding error.
- ///
- /// The property the error occurred on.
- /// The binding error.
- void LogError(AvaloniaProperty property, Exception e);
-
- ///
- /// Ensures that the current thread is the UI thread.
- ///
- void VerifyAccess();
- }
-}
diff --git a/src/Avalonia.Base/IStyledPropertyAccessor.cs b/src/Avalonia.Base/IStyledPropertyAccessor.cs
index f2ec5bd33ff..dfa0208c38e 100644
--- a/src/Avalonia.Base/IStyledPropertyAccessor.cs
+++ b/src/Avalonia.Base/IStyledPropertyAccessor.cs
@@ -18,14 +18,5 @@ internal interface IStyledPropertyAccessor
/// The default value.
///
object GetDefaultValue(Type type);
-
- ///
- /// Gets a validation function for the property on the specified type.
- ///
- /// The type.
- ///
- /// The validation function, or null if no validation function exists.
- ///
- Func GetValidationFunc(Type type);
}
}
diff --git a/src/Avalonia.Base/IStyledPropertyMetadata.cs b/src/Avalonia.Base/IStyledPropertyMetadata.cs
index 22cda075fa6..cc92e212614 100644
--- a/src/Avalonia.Base/IStyledPropertyMetadata.cs
+++ b/src/Avalonia.Base/IStyledPropertyMetadata.cs
@@ -14,10 +14,5 @@ public interface IStyledPropertyMetadata
/// Gets the default value for the property.
///
object DefaultValue { get; }
-
- ///
- /// Gets the property's validation function.
- ///
- Func Validate { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs
deleted file mode 100644
index 7f5415c2d87..00000000000
--- a/src/Avalonia.Base/PriorityBindingEntry.cs
+++ /dev/null
@@ -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
-{
- ///
- /// A registered binding in a .
- ///
- internal class PriorityBindingEntry : IDisposable, IObserver
- {
- private readonly PriorityLevel _owner;
- private IDisposable _subscription;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The owner.
- ///
- /// The binding index. Later bindings should have higher indexes.
- ///
- public PriorityBindingEntry(PriorityLevel owner, int index)
- {
- _owner = owner;
- Index = index;
- }
-
- ///
- /// Gets the observable associated with the entry.
- ///
- public IObservable Observable { get; private set; }
-
- ///
- /// Gets a description of the binding.
- ///
- public string Description
- {
- get;
- private set;
- }
-
- ///
- /// Gets the binding entry index. Later bindings will have higher indexes.
- ///
- public int Index
- {
- get;
- }
-
- ///
- /// Gets a value indicating whether the binding has completed.
- ///
- public bool HasCompleted { get; private set; }
-
- ///
- /// The current value of the binding.
- ///
- public object Value
- {
- get;
- private set;
- }
-
- ///
- /// Starts listening to the binding.
- ///
- /// The binding.
- public void Start(IObservable binding)
- {
- Contract.Requires(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);
- }
-
- ///
- /// Ends the binding subscription.
- ///
- public void Dispose()
- {
- _subscription?.Dispose();
- }
-
- void IObserver.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.OnCompleted()
- {
- HasCompleted = true;
-
- if (Dispatcher.UIThread.CheckAccess())
- {
- _owner.Completed(this);
- }
- else
- {
- Dispatcher.UIThread.Post(() => _owner.Completed(this));
- }
- }
-
- void IObserver.OnError(Exception error)
- {
- ExceptionDispatchInfo.Capture(error).Throw();
- }
- }
-}
diff --git a/src/Avalonia.Base/PriorityLevel.cs b/src/Avalonia.Base/PriorityLevel.cs
deleted file mode 100644
index a2364083ea9..00000000000
--- a/src/Avalonia.Base/PriorityLevel.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Stores bindings for a priority level in a .
- ///
- ///
- ///
- /// Each priority level in a has a current ,
- /// a list of and a . When there are no
- /// bindings present, or all bindings return then
- /// Value will equal DirectValue.
- ///
- ///
- /// When there are bindings present, then the latest added binding that doesn't return
- /// UnsetValue will take precedence. The active binding is returned by the
- /// property (which refers to the active binding's
- /// property rather than the index in
- /// Bindings).
- ///
- ///
- /// If DirectValue is set while a binding is active, then it will replace the
- /// current value until the active binding fires again.
- ///
- ///
- internal class PriorityLevel
- {
- private object _directValue;
- private int _nextIndex;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The owner.
- /// The priority.
- public PriorityLevel(
- PriorityValue owner,
- int priority)
- {
- Contract.Requires(owner != null);
-
- Owner = owner;
- Priority = priority;
- Value = _directValue = AvaloniaProperty.UnsetValue;
- ActiveBindingIndex = -1;
- Bindings = new LinkedList();
- }
-
- ///
- /// Gets the owner of the level.
- ///
- public PriorityValue Owner { get; }
-
- ///
- /// Gets the priority of this level.
- ///
- public int Priority { get; }
-
- ///
- /// Gets or sets the direct value for this priority level.
- ///
- public object DirectValue
- {
- get
- {
- return _directValue;
- }
-
- set
- {
- Value = _directValue = value;
- Owner.LevelValueChanged(this);
- }
- }
-
- ///
- /// Gets the current binding for the priority level.
- ///
- public object Value { get; private set; }
-
- ///
- /// Gets the value of the active binding, or -1
- /// if no binding is active.
- ///
- public int ActiveBindingIndex { get; private set; }
-
- ///
- /// Gets the bindings for the priority level.
- ///
- public LinkedList Bindings { get; }
-
- ///
- /// Adds a binding.
- ///
- /// The binding to add.
- /// A disposable used to remove the binding.
- public IDisposable Add(IObservable binding)
- {
- Contract.Requires(binding != null);
-
- var entry = new PriorityBindingEntry(this, _nextIndex++);
- var node = Bindings.AddFirst(entry);
-
- entry.Start(binding);
-
- return new RemoveBindingDisposable(node, Bindings, this);
- }
-
- ///
- /// Invoked when an entry in changes value.
- ///
- /// The entry that changed.
- 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();
- }
- }
- }
-
- ///
- /// Invoked when an entry in completes.
- ///
- /// The entry that completed.
- public void Completed(PriorityBindingEntry entry)
- {
- Bindings.Remove(entry);
-
- if (entry.Index >= ActiveBindingIndex)
- {
- ActivateFirstBinding();
- }
- }
-
- ///
- /// Invoked when an entry in encounters a recoverable error.
- ///
- /// The entry that completed.
- /// The error.
- public void Error(PriorityBindingEntry entry, BindingNotification error)
- {
- Owner.LevelError(this, error);
- }
-
- ///
- /// Activates the first binding that has a value.
- ///
- 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 _bindings;
- private readonly PriorityLevel _priorityLevel;
- private LinkedListNode _binding;
-
- public RemoveBindingDisposable(
- LinkedListNode binding,
- LinkedList bindings,
- PriorityLevel priorityLevel)
- {
- _binding = binding;
- _bindings = bindings;
- _priorityLevel = priorityLevel;
- }
-
- public void Dispose()
- {
- LinkedListNode 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();
- }
- }
- }
- }
- }
-}
diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs
deleted file mode 100644
index 61184ef7b13..00000000000
--- a/src/Avalonia.Base/PriorityValue.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Maintains a list of prioritized bindings together with a current value.
- ///
- ///
- /// Bindings, in the form of s are added to the object using
- /// the method. With the observable is passed a priority, where lower values
- /// represent higher priorities. The current is selected from the highest
- /// priority binding that doesn't return . 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
- /// method on the
- /// owner object is fired with the old and new values.
- ///
- internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)>
- {
- private readonly Type _valueType;
- private readonly SingleOrDictionary _levels = new SingleOrDictionary();
- private readonly Func _validate;
- private (object value, int priority) _value;
- private DeferredSetter _setter;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The owner of the object.
- /// The property that the value represents.
- /// The value type.
- /// An optional validation function.
- public PriorityValue(
- IPriorityValueOwner owner,
- AvaloniaProperty property,
- Type valueType,
- Func validate = null)
- {
- Owner = owner;
- Property = property;
- _valueType = valueType;
- _value = (AvaloniaProperty.UnsetValue, int.MaxValue);
- _validate = validate;
- }
-
- ///
- /// Gets a value indicating whether the property is animating.
- ///
- public bool IsAnimating
- {
- get
- {
- return ValuePriority <= (int)BindingPriority.Animation &&
- GetLevel(ValuePriority).ActiveBindingIndex != -1;
- }
- }
-
- ///
- /// Gets the owner of the value.
- ///
- public IPriorityValueOwner Owner { get; }
-
- ///
- /// Gets the property that the value represents.
- ///
- public AvaloniaProperty Property { get; }
-
- ///
- /// Gets the current value.
- ///
- public object Value => _value.value;
-
- ///
- /// Gets the priority of the binding that is currently active.
- ///
- public int ValuePriority => _value.priority;
-
- ///
- /// Adds a new binding.
- ///
- /// The binding.
- /// The binding priority.
- ///
- /// A disposable that will remove the binding.
- ///
- public IDisposable Add(IObservable binding, int priority)
- {
- return GetLevel(priority).Add(binding);
- }
-
- ///
- /// Sets the value for a specified priority.
- ///
- /// The value.
- /// The priority
- public void SetValue(object value, int priority)
- {
- GetLevel(priority).DirectValue = value;
- }
-
- ///
- /// Gets the currently active bindings on this object.
- ///
- /// An enumerable collection of bindings.
- public IEnumerable GetBindings()
- {
- foreach (var level in _levels)
- {
- foreach (var binding in level.Value.Bindings)
- {
- yield return binding;
- }
- }
- }
-
- ///
- /// Returns diagnostic string that can help the user debug the bindings in effect on
- /// this object.
- ///
- /// A diagnostic string.
- 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();
- }
-
- ///
- /// Called when the value for a priority level changes.
- ///
- /// The priority level of the changed entry.
- 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);
- }
- }
- }
-
- ///
- /// Called when a priority level encounters an error.
- ///
- /// The priority level of the changed entry.
- /// The binding error.
- public void LevelError(PriorityLevel level, BindingNotification error)
- {
- Owner.LogError(Property, error.Error);
- }
-
- ///
- /// Causes a revalidation of the value.
- ///
- public void Revalidate()
- {
- if (_validate != null)
- {
- PriorityLevel level;
-
- if (_levels.TryGetValue(ValuePriority, out level))
- {
- UpdateValue(level.Value, level.Priority);
- }
- }
- }
-
- ///
- /// Gets the with the specified priority, creating it if it
- /// doesn't already exist.
- ///
- /// The priority.
- /// The priority level.
- private PriorityLevel GetLevel(int priority)
- {
- PriorityLevel result;
-
- if (!_levels.TryGetValue(priority, out result))
- {
- result = new PriorityLevel(this, priority);
- _levels.Add(priority, result);
- }
-
- return result;
- }
-
- ///
- /// Updates the current and notifies all subscribers.
- ///
- /// The value to set.
- /// The priority level that the value came from.
- 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());
- }
- }
- }
-}
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
new file mode 100644
index 00000000000..09a0f169df2
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
@@ -0,0 +1,107 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Threading;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+ ///
+ /// Represents an untyped interface to .
+ ///
+ internal interface IBindingEntry : IPriorityValueEntry, IDisposable
+ {
+ }
+
+ ///
+ /// Stores a binding in a or .
+ ///
+ /// The property type.
+ internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserver>
+ {
+ private readonly IAvaloniaObject _owner;
+ private IValueSink _sink;
+ private IDisposable? _subscription;
+
+ public BindingEntry(
+ IAvaloniaObject owner,
+ StyledPropertyBase property,
+ IObservable> source,
+ BindingPriority priority,
+ IValueSink sink)
+ {
+ _owner = owner;
+ Property = property;
+ Source = source;
+ Priority = priority;
+ _sink = sink;
+ }
+
+ public StyledPropertyBase Property { get; }
+ public BindingPriority Priority { get; }
+ public IObservable> Source { get; }
+ public Optional Value { get; private set; }
+ Optional 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 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 value)
+ {
+ if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
+ {
+ value = Property.GetDefaultValue(_owner.GetType());
+ }
+
+ if (value.Type == BindingValueType.DoNothing)
+ {
+ return;
+ }
+
+ var old = Value;
+
+ if (value.Type != BindingValueType.DataValidationError)
+ {
+ Value = value.ToOptional();
+ }
+
+ _sink.ValueChanged(Property, Priority, old, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
new file mode 100644
index 00000000000..f15f56e32b7
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
@@ -0,0 +1,33 @@
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+ ///
+ /// Stores a value with a priority in a or
+ /// .
+ ///
+ /// The property type.
+ internal class ConstantValueEntry : IPriorityValueEntry
+ {
+ public ConstantValueEntry(
+ StyledPropertyBase property,
+ T value,
+ BindingPriority priority)
+ {
+ Property = property;
+ Value = value;
+ Priority = priority;
+ }
+
+ public StyledPropertyBase Property { get; }
+ public BindingPriority Priority { get; }
+ public Optional Value { get; }
+ Optional IValue.Value => Value.ToObject();
+ BindingPriority IValue.ValuePriority => Priority;
+
+ public void Reparent(IValueSink sink) { }
+ }
+}
diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
new file mode 100644
index 00000000000..6ed6c2ef52f
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
@@ -0,0 +1,25 @@
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+ ///
+ /// Represents an untyped interface to .
+ ///
+ internal interface IPriorityValueEntry : IValue
+ {
+ BindingPriority Priority { get; }
+
+ void Reparent(IValueSink sink);
+ }
+
+ ///
+ /// Represents an object that can act as an entry in a .
+ ///
+ /// The property type.
+ internal interface IPriorityValueEntry : IPriorityValueEntry, IValue
+ {
+ }
+}
diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs
new file mode 100644
index 00000000000..0ce7fb83088
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/IValue.cs
@@ -0,0 +1,24 @@
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+ ///
+ /// Represents an untyped interface to .
+ ///
+ internal interface IValue
+ {
+ Optional Value { get; }
+ BindingPriority ValuePriority { get; }
+ }
+
+ ///
+ /// Represents an object that can act as an entry in a .
+ ///
+ /// The property type.
+ internal interface IValue : IValue
+ {
+ new Optional Value { get; }
+ }
+}
diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs
new file mode 100644
index 00000000000..223b0058c1d
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs
@@ -0,0 +1,20 @@
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+ ///
+ /// Represents an entity that can receive change notifications in a .
+ ///
+ internal interface IValueSink
+ {
+ void ValueChanged(
+ StyledPropertyBase property,
+ BindingPriority priority,
+ Optional oldValue,
+ BindingValue newValue);
+
+ void Completed(AvaloniaProperty property, IPriorityValueEntry entry);
+ }
+}
diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
new file mode 100644
index 00000000000..22258390dab
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
@@ -0,0 +1,22 @@
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+ ///
+ /// Stores a value with local value priority in a or
+ /// .
+ ///
+ /// The property type.
+ internal class LocalValueEntry : IValue
+ {
+ private T _value;
+
+ public LocalValueEntry(T value) => _value = value;
+ public Optional Value => _value;
+ public BindingPriority ValuePriority => BindingPriority.LocalValue;
+ Optional IValue.Value => Value.ToObject();
+ public void SetValue(T value) => _value = value;
+ }
+}
diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
new file mode 100644
index 00000000000..2785dc6840c
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+ ///
+ /// Stores a set of prioritized values and bindings in a