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 source, + public IDisposable Bind( + StyledPropertyBase property, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - Contract.Requires(source != null); - + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); - if (property.IsDirect) - { - if (property.IsReadOnly) - { - throw new ArgumentException($"The property {property.Name} is readonly."); - } - - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority LocalValue", - property, - GetDescription(source)); - - if (_directBindings == null) - { - _directBindings = new List(); - } - - return new DirectBindingSubscription(this, property, source); - } - else - { - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority {Priority}", - property, - GetDescription(source), - priority); - - return Values.AddBinding(property, source, priority); - } + return Values.AddBinding(property, source, priority); } /// @@ -374,42 +383,90 @@ public 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. /// public IDisposable Bind( - AvaloniaProperty property, - IObservable source, - BindingPriority priority = BindingPriority.LocalValue) + DirectPropertyBase property, + IObservable> source) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + VerifyAccess(); + + property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + + if (property.IsReadOnly) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } + + Logger.TryGet(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "Bound {Property} to {Binding} with priority LocalValue", + property, + GetDescription(source)); + + _directBindings ??= new List(); - return Bind(property, source.Select(x => (object)x), priority); + return new DirectBindingSubscription(this, property, source); } /// - /// Forces the specified property to be revalidated. + /// Coerces the specified . /// + /// The type of the property. /// The property. - public void Revalidate(AvaloniaProperty property) + public void CoerceValue(StyledPropertyBase property) { - VerifyAccess(); - _values?.Revalidate(property); + _values?.CoerceValue(property); + } + + /// + void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) + { + _inheritanceChildren ??= new List(); + _inheritanceChildren.Add(child); } - internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue) + /// + void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child) + { + _inheritanceChildren?.Remove(child); + } + + void IAvaloniaObject.InheritedPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + Optional newValue) + { + if (property.Inherits && (_values == null || !_values.IsSet(property))) + { + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); + } + } + + /// + Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() { - oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? - GetDefaultValue(property) : - oldValue; - newValue = (newValue == AvaloniaProperty.UnsetValue) ? - GetDefaultValue(property) : - newValue; + return _propertyChanged?.GetInvocationList(); + } - if (!Equals(oldValue, newValue)) + void IValueSink.ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue) + { + oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property); + newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property)); + + LogIfError(property, newValue); + + if (!EqualityComparer.Default.Equals(oldValue.Value, newValue.Value)) { - RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); + RaisePropertyChanged(property, oldValue, newValue, priority); Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, @@ -421,39 +478,59 @@ internal void PriorityValueChanged(AvaloniaProperty property, int priority, obje (BindingPriority)priority); } } - - internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) - { - LogIfError(property, notification); - UpdateDataValidation(property, notification); - } - /// - Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() - { - return _propertyChanged?.GetInvocationList(); - } + void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { } /// - /// Gets all priority values set on the object. + /// Called for each inherited property when the changes. /// - /// A collection of property/value tuples. - internal IDictionary GetSetValues() => Values?.GetSetValues(); + /// The type of the property value. + /// The property. + /// The old inheritance parent. + internal void InheritanceParentChanged( + StyledPropertyBase property, + IAvaloniaObject oldParent) + { + var oldValue = oldParent switch + { + AvaloniaObject o => o.GetValueOrInheritedOrDefault(property), + null => property.GetDefaultValue(GetType()), + _ => oldParent.GetValue(property) + }; - /// - /// Forces revalidation of properties when a property value changes. - /// - /// The property to that affects validation. - /// The affected properties. - protected static void AffectsValidation(AvaloniaProperty property, params AvaloniaProperty[] affected) + var newValue = GetInheritedOrDefault(property); + + if (!EqualityComparer.Default.Equals(oldValue, newValue)) + { + RaisePropertyChanged(property, oldValue, newValue); + } + } + + internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property) { - property.Changed.Subscribe(e => + if (property.IsDirect) { - foreach (var p in affected) + return new AvaloniaPropertyValue( + property, + GetValue(property), + BindingPriority.Unset, + "Local Value"); + } + else if (_values != null) + { + var result = _values.GetDiagnostic(property); + + if (result != null) { - e.Sender.Revalidate(p); + return result; } - }); + } + + return new AvaloniaPropertyValue( + property, + GetValue(property), + BindingPriority.Unset, + "Unset"); } /// @@ -477,18 +554,25 @@ protected internal virtual void LogBindingError(AvaloniaProperty property, Excep /// enabled. /// /// The property. - /// The new validation status. - protected virtual void UpdateDataValidation( - AvaloniaProperty property, - BindingNotification status) + /// The new binding value for the property. + protected virtual void UpdateDataValidation( + AvaloniaProperty property, + BindingValue value) { } /// /// Called when a avalonia property changes on the object. /// - /// The event arguments. - protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + /// The property whose value has changed. + /// The old value of the property. + /// The new value of the property. + /// The priority of the new value. + protected virtual void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) { } @@ -499,40 +583,57 @@ protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected internal void RaisePropertyChanged( - AvaloniaProperty property, - object oldValue, - object newValue, + protected internal void RaisePropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - VerifyAccess(); + property = property ?? throw new ArgumentNullException(nameof(property)); - AvaloniaPropertyChangedEventArgs e = new AvaloniaPropertyChangedEventArgs( - this, - property, - oldValue, - newValue, - priority); + VerifyAccess(); property.Notifying?.Invoke(this, true); try { - OnPropertyChanged(e); - property.NotifyChanged(e); + AvaloniaPropertyChangedEventArgs e = null; + var hasChanged = property.HasChangedSubscriptions; + + if (hasChanged || _propertyChanged != null) + { + e = new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + newValue, + priority); + } + + OnPropertyChanged(property, oldValue, newValue, priority); + + if (hasChanged) + { + property.NotifyChanged(e); + } _propertyChanged?.Invoke(this, e); if (_inpcChanged != null) { - PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); - _inpcChanged(this, e2); + var inpce = new PropertyChangedEventArgs(property.Name); + _inpcChanged(this, inpce); } - if (property.Inherits) + if (property.Inherits && _inheritanceChildren != null) { - _inheritablePropertyChanged?.Invoke(this, e); + foreach (var child in _inheritanceChildren) + { + child.InheritedPropertyChanged( + property, + oldValue, + newValue.ToOptional()); + } } } finally @@ -561,216 +662,103 @@ protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value return false; } - DeferredSetter setter = Values.GetDirectDeferredSetter(property); - - return setter.SetAndNotify(this, property, ref field, value); + var old = field; + field = value; + RaisePropertyChanged(property, old, value); + return true; } - /// - /// Tries to cast a value to a type, taking into account that the value may be a - /// . - /// - /// The value. - /// The type. - /// The cast value, or a . - private static object CastOrDefault(object value, Type type) + private T GetInheritedOrDefault(StyledPropertyBase property) { - var notification = value as BindingNotification; - - if (notification == null) + if (property.Inherits && InheritanceParent is AvaloniaObject o) { - return TypeUtilities.ConvertImplicitOrDefault(value, type); + return o.GetValueOrInheritedOrDefault(property); } - else - { - if (notification.HasValue) - { - notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type)); - } - return notification; - } + return property.GetDefaultValue(GetType()); } - /// - /// Gets the default value for a property. - /// - /// The property. - /// The default value. - private object GetDefaultValue(AvaloniaProperty property) - { - if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValueOrDefaultUnchecked(property); - return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); - } - - /// - /// Gets the value or default value for a property. - /// - /// The property. - /// The default value. - private object GetValueOrDefaultUnchecked(AvaloniaProperty property) + private T GetValueOrInheritedOrDefault(StyledPropertyBase property) { - var aobj = this; - var valuestore = aobj._values; - if (valuestore != null) - { - var result = valuestore.GetValue(property); - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - } - if (property.Inherits) - { - while (aobj.InheritanceParent is AvaloniaObject parent) - { - aobj = parent; - valuestore = aobj._values; - if (valuestore != null) - { - var result = valuestore.GetValue(property); - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - } - } - } - return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - } + var o = this; + var inherits = property.Inherits; + var value = default(T); - /// - /// Sets the value of a direct property. - /// - /// The property. - /// The value. - private void SetDirectValue(AvaloniaProperty property, object value) - { - void Set() + while (o != null) { - var notification = value as BindingNotification; + var values = o._values; - if (notification != null) + if (values?.TryGetValue(property, out value) == true) { - LogIfError(property, notification); - value = notification.Value; + return value; } - if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue) + if (!inherits) { - var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType()); - var accessor = (IDirectPropertyAccessor)GetRegistered(property); - var finalValue = value == AvaloniaProperty.UnsetValue ? - metadata.UnsetValue : value; - - LogPropertySet(property, value, BindingPriority.LocalValue); - - accessor.SetValue(this, finalValue); + break; } - if (notification != null) - { - UpdateDataValidation(property, notification); - } + o = o.InheritanceParent as AvaloniaObject; } - if (Dispatcher.UIThread.CheckAccess()) - { - Set(); - } - else - { - Dispatcher.UIThread.Post(Set); - } + return property.GetDefaultValue(GetType()); } /// - /// Sets the value of a styled property. + /// Sets the value of a direct property. /// /// The property. /// The value. - /// The priority of the value. - private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority) + private void SetDirectValueUnchecked(DirectPropertyBase property, T value) { - var notification = value as BindingNotification; + var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); - // We currently accept BindingNotifications for non-direct properties but we just - // strip them to their underlying value. - if (notification != null) + if (value is UnsetValueType) { - if (!notification.HasValue) - { - return; - } - else - { - value = notification.Value; - } + p.InvokeSetter(this, p.GetUnsetValue(GetType())); } - - var originalValue = value; - - if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value)) + else if (!(value is DoNothingType)) { - throw new ArgumentException(string.Format( - "Invalid value for Property '{0}': '{1}' ({2})", - property.Name, - originalValue, - originalValue?.GetType().FullName ?? "(null)")); + p.InvokeSetter(this, value); } - - LogPropertySet(property, value, priority); - Values.AddValue(property, value, (int)priority); } /// - /// Given a direct property, returns a registered avalonia property that is equivalent or - /// throws if not found. + /// Sets the value of a direct property. /// /// The property. - /// The registered property. - private AvaloniaProperty GetRegistered(AvaloniaProperty property) + /// The value. + private void SetDirectValueUnchecked(DirectPropertyBase property, BindingValue value) { - var direct = property as IDirectPropertyAccessor; - - if (direct == null) - { - throw new AvaloniaInternalException( - "AvaloniaObject.GetRegistered should only be called for direct properties"); - } + var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property); - if (property.OwnerType.IsAssignableFrom(GetType())) + if (p == null) { - return property; + throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); } - var result = AvaloniaPropertyRegistry.Instance.GetRegistered(this) - .FirstOrDefault(x => x == property); + LogIfError(property, value); - if (result == null) + switch (value.Type) { - throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); + case BindingValueType.UnsetValue: + case BindingValueType.BindingError: + var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType())); + property.InvokeSetter(this, fallback); + break; + case BindingValueType.DataValidationError: + property.InvokeSetter(this, value); + break; + case BindingValueType.Value: + case BindingValueType.BindingErrorWithFallback: + case BindingValueType.DataValidationErrorWithFallback: + property.InvokeSetter(this, value); + break; } - return result; - } - - /// - /// Called when a property is changed on the current . - /// - /// The event sender. - /// The event args. - /// - /// Checks for changes in an inherited property value. - /// - private void ParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - Contract.Requires(e != null); - - if (e.Property.Inherits && !IsSet(e.Property)) + if (p.IsDataValidationEnabled) { - RaisePropertyChanged(e.Property, e.OldValue, e.NewValue, BindingPriority.LocalValue); + UpdateDataValidation(property, value); } } @@ -779,7 +767,7 @@ private void ParentPropertyChanged(object sender, AvaloniaPropertyChangedEventAr /// /// The observable. /// The description. - private string GetDescription(IObservable o) + private string GetDescription(object o) { var description = o as IDescription; return description?.Description ?? o.ToString(); @@ -789,12 +777,12 @@ private string GetDescription(IObservable o) /// Logs a mesage if the notification represents a binding error. /// /// 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 . + /// + /// The property type. + /// + /// When more than a single value or binding is applied to a property in an + /// , the entry in the is converted into + /// a . This class holds any number of + /// entries (sorted first by priority and then in the order + /// they were added) plus a local value. + /// + internal class PriorityValue : IValue, IValueSink + { + private readonly IAvaloniaObject _owner; + private readonly IValueSink _sink; + private readonly List> _entries = new List>(); + private readonly Func? _coerceValue; + private Optional _localValue; + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink) + { + _owner = owner; + Property = property; + _sink = sink; + + if (property.HasCoercion) + { + var metadata = property.GetMetadata(owner.GetType()); + _coerceValue = metadata.CoerceValue; + } + } + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink, + IPriorityValueEntry existing) + : this(owner, property, sink) + { + existing.Reparent(this); + _entries.Add(existing); + + if (existing.Value.HasValue) + { + Value = existing.Value; + ValuePriority = existing.Priority; + } + } + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink, + LocalValueEntry existing) + : this(owner, property, sink) + { + _localValue = existing.Value; + Value = _localValue; + ValuePriority = BindingPriority.LocalValue; + } + + public StyledPropertyBase Property { get; } + public Optional Value { get; private set; } + public BindingPriority ValuePriority { get; private set; } + public IReadOnlyList> Entries => _entries; + Optional IValue.Value => Value.ToObject(); + + public void ClearLocalValue() => UpdateEffectiveValue(); + + public void SetValue(T value, BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + _localValue = value; + } + else + { + var insert = FindInsertPoint(priority); + _entries.Insert(insert, new ConstantValueEntry(Property, value, priority)); + } + + UpdateEffectiveValue(); + } + + public BindingEntry AddBinding(IObservable> source, BindingPriority priority) + { + var binding = new BindingEntry(_owner, Property, source, priority, this); + var insert = FindInsertPoint(binding.Priority); + _entries.Insert(insert, binding); + return binding; + } + + public void CoerceValue() => UpdateEffectiveValue(); + + void IValueSink.ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue) + { + if (priority == BindingPriority.LocalValue) + { + _localValue = default; + } + + UpdateEffectiveValue(); + } + + void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) + { + _entries.Remove((IPriorityValueEntry)entry); + UpdateEffectiveValue(); + } + + private int FindInsertPoint(BindingPriority priority) + { + var result = _entries.Count; + + for (var i = 0; i < _entries.Count; ++i) + { + if (_entries[i].Priority < priority) + { + result = i; + break; + } + } + + return result; + } + + private void UpdateEffectiveValue() + { + var reachedLocalValues = false; + var value = default(Optional); + + if (_entries.Count > 0) + { + for (var i = _entries.Count - 1; i >= 0; --i) + { + var entry = _entries[i]; + + if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) + { + reachedLocalValues = true; + + if (_localValue.HasValue) + { + value = _localValue; + ValuePriority = BindingPriority.LocalValue; + break; + } + } + + if (entry.Value.HasValue) + { + value = entry.Value; + ValuePriority = entry.Priority; + break; + } + } + } + else if (_localValue.HasValue) + { + value = _localValue; + ValuePriority = BindingPriority.LocalValue; + } + + if (value.HasValue && _coerceValue != null) + { + value = _coerceValue(_owner, value.Value); + } + + if (value != Value) + { + var old = Value; + Value = value; + _sink.ValueChanged(Property, ValuePriority, old, value); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs new file mode 100644 index 00000000000..be044b05591 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + private T _value; + +#nullable disable + public AvaloniaPropertyBindingObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(target); + _property = property; + } +#nullable enable + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + _value = (T)target.GetValue(_property); + target.PropertyChanged += PropertyChanged; + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } + } + + protected override void Subscribed(IObserver> observer, bool first) + { + observer.OnNext(new BindingValue(_value)); + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + var newValue = e.Sender.GetValue(typedArgs.Property); + + if (!typedArgs.OldValue.HasValue || !EqualityComparer.Default.Equals(newValue, _value)) + { + _value = newValue; + PublishNext(_value); + } + } + else + { + var newValue = e.Sender.GetValue(e.Property); + + if (!Equals(newValue, _value)) + { + _value = (T)newValue; + PublishNext(_value); + } + } + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index 4385ab13ef3..238aba5c962 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -44,8 +44,22 @@ private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _property) { - _value = (T)e.NewValue; - PublishNext(_value); + T newValue; + + if (e is AvaloniaPropertyChangedEventArgs typed) + { + newValue = typed.Sender.GetValue(typed.Property); + } + else + { + newValue = (T)e.Sender.GetValue(e.Property); + } + + if (!Equals(newValue, _value)) + { + _value = (T)newValue; + PublishNext(_value); + } } } } diff --git a/src/Avalonia.Base/Reactive/BindingValueAdapter.cs b/src/Avalonia.Base/Reactive/BindingValueAdapter.cs new file mode 100644 index 00000000000..8c80e9f48cf --- /dev/null +++ b/src/Avalonia.Base/Reactive/BindingValueAdapter.cs @@ -0,0 +1,61 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class BindingValueAdapter : SingleSubscriberObservableBase>, + IObserver + { + private readonly IObservable _source; + private IDisposable? _subscription; + + public BindingValueAdapter(IObservable source) => _source = source; + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + public void OnNext(T value) => PublishNext(BindingValue.FromUntyped(value)); + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } + + internal class BindingValueSubjectAdapter : SingleSubscriberObservableBase>, + ISubject> + { + private readonly ISubject _source; + private readonly Inner _inner; + private IDisposable? _subscription; + + public BindingValueSubjectAdapter(ISubject source) + { + _source = source; + _inner = new Inner(this); + } + + public void OnCompleted() => _source.OnCompleted(); + public void OnError(Exception error) => _source.OnError(error); + + public void OnNext(BindingValue value) + { + if (value.HasValue) + { + _source.OnNext(value.Value); + } + } + + protected override void Subscribed() => _subscription = _source.Subscribe(_inner); + protected override void Unsubscribed() => _subscription?.Dispose(); + + private class Inner : IObserver + { + private readonly BindingValueSubjectAdapter _owner; + + public Inner(BindingValueSubjectAdapter owner) => _owner = owner; + + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + public void OnNext(T value) => _owner.PublishNext(BindingValue.FromUntyped(value)); + } + } +} diff --git a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs new file mode 100644 index 00000000000..6f0d29dd0ff --- /dev/null +++ b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + public static class BindingValueExtensions + { + public static IObservable> ToBindingValue(this IObservable source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new BindingValueAdapter(source); + } + + public static ISubject> ToBindingValue(this ISubject source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new BindingValueSubjectAdapter(source); + } + + public static IObservable ToUntyped(this IObservable> source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new UntypedBindingAdapter(source); + } + + public static ISubject ToUntyped(this ISubject> source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new UntypedBindingSubjectAdapter(source); + } + } +} diff --git a/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs new file mode 100644 index 00000000000..bd9b31b1007 --- /dev/null +++ b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia.Data; +using Avalonia.Logging; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class TypedBindingAdapter : SingleSubscriberObservableBase>, + IObserver> + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + private readonly IObservable> _source; + private IDisposable? _subscription; + + public TypedBindingAdapter( + IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source) + { + _target = target; + _property = property; + _source = source; + } + + public void OnNext(BindingValue value) + { + try + { + PublishNext(value.Convert()); + } + catch (InvalidCastException e) + { + Logger.TryGet(LogEventLevel.Error)?.Log( + LogArea.Binding, + _target, + "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", + _property.Name, + _property.PropertyType, + value.Value, + value.Value?.GetType()); + PublishNext(BindingValue.BindingError(e)); + } + } + + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + + public static IObservable> Create( + IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source) + { + return source is IObservable> result ? + result : + new TypedBindingAdapter(target, property, source); + } + + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } +} diff --git a/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs new file mode 100644 index 00000000000..03c1afcea91 --- /dev/null +++ b/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs @@ -0,0 +1,57 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class UntypedBindingAdapter : SingleSubscriberObservableBase, + IObserver> + { + private readonly IObservable> _source; + private IDisposable? _subscription; + + public UntypedBindingAdapter(IObservable> source) => _source = source; + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + public void OnNext(BindingValue value) => value.ToUntyped(); + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } + + internal class UntypedBindingSubjectAdapter : SingleSubscriberObservableBase, + ISubject + { + private readonly ISubject> _source; + private readonly Inner _inner; + private IDisposable? _subscription; + + public UntypedBindingSubjectAdapter(ISubject> source) + { + _source = source; + _inner = new Inner(this); + } + + public void OnCompleted() => _source.OnCompleted(); + public void OnError(Exception error) => _source.OnError(error); + public void OnNext(object? value) + { + _source.OnNext(BindingValue.FromUntyped(value)); + } + + protected override void Subscribed() => _subscription = _source.Subscribe(_inner); + protected override void Unsubscribed() => _subscription?.Dispose(); + + private class Inner : IObserver> + { + private readonly UntypedBindingSubjectAdapter _owner; + + public Inner(UntypedBindingSubjectAdapter owner) => _owner = owner; + + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + public void OnNext(BindingValue value) => _owner.PublishNext(value.ToUntyped()); + } + } +} diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 4eb85a046e6..62443b424cd 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -17,14 +17,16 @@ public class StyledProperty : StyledPropertyBase /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. /// A callback. public StyledProperty( string name, Type ownerType, StyledPropertyMetadata metadata, bool inherits = false, + Func validate = null, Action notifying = null) - : base(name, ownerType, metadata, inherits, notifying) + : base(name, ownerType, metadata, inherits, validate, notifying) { } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 27a502246a6..8c4d683ae00 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -3,13 +3,15 @@ using System; using System.Diagnostics; +using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia { /// /// Base class for styled properties. /// - public class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor + public abstract class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor { private bool _inherits; @@ -20,12 +22,14 @@ public class StyledPropertyBase : AvaloniaProperty, IStyledPrope /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. /// A callback. protected StyledPropertyBase( string name, Type ownerType, StyledPropertyMetadata metadata, bool inherits = false, + Func validate = null, Action notifying = null) : base(name, ownerType, metadata, notifying) { @@ -38,6 +42,14 @@ protected StyledPropertyBase( } _inherits = inherits; + ValidateValue = validate; + HasCoercion |= metadata.CoerceValue != null; + + if (validate?.Invoke(metadata.DefaultValue) == false) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); + } } /// @@ -59,6 +71,29 @@ protected StyledPropertyBase(StyledPropertyBase source, Type ownerType) /// public override bool Inherits => _inherits; + /// + /// Gets the value validation callback for the property. + /// + public Func ValidateValue { get; } + + /// + /// Gets a value indicating whether this property has any value coercion callbacks defined + /// in its metadata. + /// + internal bool HasCoercion { get; private set; } + + public TValue CoerceValue(IAvaloniaObject instance, TValue baseValue) + { + var metadata = GetMetadata(instance.GetType()); + + if (metadata.CoerceValue != null) + { + return metadata.CoerceValue.Invoke(instance, baseValue); + } + + return baseValue; + } + /// /// Gets the default value for the property on the specified type. /// @@ -68,7 +103,7 @@ public TValue GetDefaultValue(Type type) { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Typed; + return GetMetadata(type).DefaultValue; } /// @@ -120,57 +155,104 @@ public void OverrideMetadata(StyledPropertyMetadata metadata) where T /// The metadata. public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) { + if (ValidateValue != null) + { + if (!ValidateValue(metadata.DefaultValue)) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); + } + } + + HasCoercion |= metadata.CoerceValue != null; + base.OverrideMetadata(type, metadata); } /// - /// Overrides the validation function for the specified type. + /// Gets the string representation of the property. /// - /// The type. - /// The validation function. - public void OverrideValidation(Func validate) - where THost : IAvaloniaObject + /// The property's string representation. + public override string ToString() { - Func f; + return Name; + } - if (validate != null) - { - f = Cast(validate); - } - else + /// + object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + /// + internal override void NotifyInitialized(IAvaloniaObject o) + { + if (HasNotifyInitializedObservers) { - // Passing null to the validation function means that the property metadata merge - // will take the base validation function, so instead use an empty validation. - f = (o, v) => v; + var e = new AvaloniaPropertyChangedEventArgs( + o, + this, + default, + o.GetValue(this), + BindingPriority.Unset); + NotifyInitialized(e); } + } - base.OverrideMetadata(typeof(THost), new StyledPropertyMetadata(validate: f)); + /// + internal override void RouteClearValue(IAvaloniaObject o) + { + o.ClearValue(this); } - /// - /// Gets the string representation of the property. - /// - /// The property's string representation. - public override string ToString() + /// + internal override object RouteGetValue(IAvaloniaObject o) { - return Name; + return o.GetValue(this); } /// - Func IStyledPropertyAccessor.GetValidationFunc(Type type) + internal override void RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority) { - Contract.Requires(type != null); - return ((IStyledPropertyMetadata)base.GetMetadata(type)).Validate; + var v = TryConvert(value); + + if (v.HasValue) + { + o.SetValue(this, (TValue)v.Value, priority); + } + else if (v.Type == BindingValueType.UnsetValue) + { + o.ClearValue(this); + } + else if (v.HasError) + { + throw v.Error; + } } /// - object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + internal override IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority) + { + var adapter = TypedBindingAdapter.Create(o, this, source); + return o.Bind(this, adapter, priority); + } + + /// + internal override void RouteInheritanceParentChanged( + AvaloniaObject o, + IAvaloniaObject oldParent) + { + o.InheritanceParentChanged(this, oldParent); + } private object GetDefaultBoxedValue(Type type) { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Boxed; + return GetMetadata(type).DefaultValue; } [DebuggerHidden] diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index d1a0e2dc533..f96298a298f 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -12,35 +12,35 @@ namespace Avalonia /// public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata { + private Optional _defaultValue; + /// /// Initializes a new instance of the class. /// /// The default value of the property. - /// A validation function. /// The default binding mode. + /// A value coercion callback. public StyledPropertyMetadata( - TValue defaultValue = default, - Func validate = null, - BindingMode defaultBindingMode = BindingMode.Default) + Optional defaultValue = default, + BindingMode defaultBindingMode = BindingMode.Default, + Func coerce = null) : base(defaultBindingMode) { - DefaultValue = new BoxedValue(defaultValue); - Validate = validate; + _defaultValue = defaultValue; + CoerceValue = coerce; } /// /// Gets the default value for the property. /// - internal BoxedValue DefaultValue { get; private set; } + public TValue DefaultValue => _defaultValue.GetValueOrDefault(); /// - /// Gets the validation callback. + /// Gets the value coercion callback, if any. /// - public Func Validate { get; private set; } - - object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed; + public Func? CoerceValue { get; private set; } - Func IStyledPropertyMetadata.Validate => Cast(Validate); + object IStyledPropertyMetadata.DefaultValue => DefaultValue; /// public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property) @@ -49,29 +49,16 @@ public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty prope if (baseMetadata is StyledPropertyMetadata src) { - if (DefaultValue.Boxed == null) + if (!_defaultValue.HasValue) { - DefaultValue = src.DefaultValue; + _defaultValue = src.DefaultValue; } - if (Validate == null) + if (CoerceValue == null) { - Validate = src.Validate; + CoerceValue = src.CoerceValue; } } } - - [DebuggerHidden] - private static Func Cast(Func f) - { - if (f == null) - { - return null; - } - else - { - return (o, v) => f(o, (TValue)v); - } - } } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index ac128d83de4..a4e7a234546 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -129,6 +129,27 @@ public void SetValue(AvaloniaProperty property, TValue value) _entries[TryFindEntry(property.Id).Item1].Value = value; } + public void Remove(AvaloniaProperty property) + { + var (index, found) = TryFindEntry(property.Id); + + if (found) + { + Entry[] entries = new Entry[_entries.Length - 1]; + int ix = 0; + + for (int i = 0; i < _entries.Length; ++i) + { + if (i != index) + { + entries[ix++] = _entries[i]; + } + } + + _entries = entries; + } + } + public Dictionary ToDictionary() { var dict = new Dictionary(_entries.Length - 1); diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs deleted file mode 100644 index fe9b0e58a0d..00000000000 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ /dev/null @@ -1,128 +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; - -namespace Avalonia.Utilities -{ - /// - /// A utility class to enable deferring assignment until after property-changed notifications are sent. - /// Used to fix #855. - /// - /// The type of value with which to track the delayed assignment. - internal sealed class DeferredSetter - { - private readonly SingleOrQueue _pendingValues; - private bool _isNotifying; - - public DeferredSetter() - { - _pendingValues = new SingleOrQueue(); - } - - private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) - { - var old = backing; - - backing = value; - - source.RaisePropertyChanged(property, old, value); - } - - public bool SetAndNotify( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value) - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - SetAndRaisePropertyChanged(source, property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - public bool SetAndNotifyCallback(AvaloniaProperty property, ISetAndNotifyHandler setAndNotifyHandler, ref TValue backing, TValue value) - where TValue : TSetRecord - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - /// - /// Disposable that marks the property as currently notifying. - /// When disposed, marks the property as done notifying. - /// - private readonly struct NotifyDisposable : IDisposable - { - private readonly DeferredSetter _setter; - - internal NotifyDisposable(DeferredSetter setter) - { - _setter = setter; - _setter._isNotifying = true; - } - - public void Dispose() - { - _setter._isNotifying = false; - } - } - } - - /// - /// Handler for set and notify requests. - /// - /// Value type. - internal interface ISetAndNotifyHandler - { - /// - /// Handles deferred setter requests to set a value. - /// - /// Property being set. - /// Backing field reference. - /// New value. - void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value); - } -} diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index d85eb4cd769..3cf05509d44 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -289,6 +289,17 @@ public static object ConvertImplicitOrDefault(object value, Type type) return TryConvertImplicit(type, value, out object result) ? result : Default(type); } + public static T ConvertImplicit(object value) + { + if (TryConvertImplicit(typeof(T), value, out var result)) + { + return (T)result; + } + + throw new InvalidCastException( + $"Unable to convert object '{value ?? "(null)"}' of type '{value?.GetType()}' to type '{typeof(T)}'."); + } + /// /// Gets the default value for the specified type. /// diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e06c5996c92..58ebc486522 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,205 +1,271 @@ using System; -using System.Collections.Generic; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Utilities; +#nullable enable + namespace Avalonia { - internal class ValueStore : IPriorityValueOwner + /// + /// Stores styled property values for an . + /// + /// + /// At its core this class consists of an to + /// mapping which holds the current values for each set property. This + /// can be in one of 4 states: + /// + /// - For a single local value it will be an instance of . + /// - For a single value of a priority other than LocalValue it will be an instance of + /// ` + /// - For a single binding it will be an instance of + /// - For all other cases it will be an instance of + /// + internal class ValueStore : IValueSink { - private readonly AvaloniaPropertyValueStore _propertyValues; - private readonly AvaloniaPropertyValueStore _deferredSetters; private readonly AvaloniaObject _owner; + private readonly IValueSink _sink; + private readonly AvaloniaPropertyValueStore _values; public ValueStore(AvaloniaObject owner) { - _owner = owner; - _propertyValues = new AvaloniaPropertyValueStore(); - _deferredSetters = new AvaloniaPropertyValueStore(); + _sink = _owner = owner; + _values = new AvaloniaPropertyValueStore(); } - public IDisposable AddBinding( - AvaloniaProperty property, - IObservable source, - BindingPriority priority) + public bool IsAnimating(AvaloniaProperty property) { - PriorityValue priorityValue; - - if (_propertyValues.TryGetValue(property, out var v)) - { - priorityValue = v as PriorityValue; - - if (priorityValue == null) - { - priorityValue = CreatePriorityValue(property); - priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValue(property, priorityValue); - } - } - else + if (_values.TryGetValue(property, out var slot)) { - priorityValue = CreatePriorityValue(property); - _propertyValues.AddValue(property, priorityValue); + return slot.ValuePriority < BindingPriority.LocalValue; } - return priorityValue.Add(source, (int)priority); + return false; } - public void AddValue(AvaloniaProperty property, object value, int priority) + public bool IsSet(AvaloniaProperty property) { - PriorityValue priorityValue; - - if (_propertyValues.TryGetValue(property, out var v)) + if (_values.TryGetValue(property, out var slot)) { - priorityValue = v as PriorityValue; - - if (priorityValue == null) - { - if (priority == (int)BindingPriority.LocalValue) - { - Validate(property, ref value); - _propertyValues.SetValue(property, value); - Changed(property, priority, v, value); - return; - } - else - { - priorityValue = CreatePriorityValue(property); - priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValue(property, priorityValue); - } - } + return slot.Value.HasValue; } - else + + return false; + } + + public bool TryGetValue(StyledPropertyBase property, out T value) + { + if (_values.TryGetValue(property, out var slot)) { - if (value == AvaloniaProperty.UnsetValue) - { - return; - } + var v = (IValue)slot; - if (priority == (int)BindingPriority.LocalValue) - { - Validate(property, ref value); - _propertyValues.AddValue(property, value); - Changed(property, priority, AvaloniaProperty.UnsetValue, value); - return; - } - else + if (v.Value.HasValue) { - priorityValue = CreatePriorityValue(property); - _propertyValues.AddValue(property, priorityValue); + value = v.Value.Value; + return true; } } - priorityValue.SetValue(value, priority); + value = default!; + return false; } - public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) + public void SetValue(StyledPropertyBase property, T value, BindingPriority priority) { - _owner.BindingNotificationReceived(property, notification); + if (property.ValidateValue?.Invoke(value) == false) + { + throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); + } + + if (_values.TryGetValue(property, out var slot)) + { + SetExisting(slot, property, value, priority); + } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + _values.AddValue(property, entry); + entry.SetValue(value, priority); + } + else if (priority == BindingPriority.LocalValue) + { + _values.AddValue(property, new LocalValueEntry(value)); + _sink.ValueChanged(property, priority, default, value); + } + else + { + var entry = new ConstantValueEntry(property, value, priority); + _values.AddValue(property, entry); + _sink.ValueChanged(property, priority, default, value); + } } - public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) { - _owner.PriorityValueChanged(property, priority, oldValue, newValue); + if (_values.TryGetValue(property, out var slot)) + { + return BindExisting(slot, property, source, priority); + } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + var binding = entry.AddBinding(source, priority); + _values.AddValue(property, entry); + binding.Start(); + return binding; + } + else + { + var entry = new BindingEntry(_owner, property, source, priority, this); + _values.AddValue(property, entry); + entry.Start(); + return entry; + } } - public IDictionary GetSetValues() + public void ClearLocalValue(StyledPropertyBase property) { - return _propertyValues.ToDictionary(); + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.ClearLocalValue(); + } + else + { + var remove = slot is ConstantValueEntry c ? + c.Priority == BindingPriority.LocalValue : + !(slot is IPriorityValueEntry); + + if (remove) + { + var old = TryGetValue(property, out var value) ? value : default; + _values.Remove(property); + _sink.ValueChanged( + property, + BindingPriority.LocalValue, + old, + BindingValue.Unset); + } + } + } } - public void LogError(AvaloniaProperty property, Exception e) + public void CoerceValue(StyledPropertyBase property) { - _owner.LogBindingError(property, e); + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.CoerceValue(); + } + } } - public object GetValue(AvaloniaProperty property) + public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) { - var result = AvaloniaProperty.UnsetValue; - - if (_propertyValues.TryGetValue(property, out var value)) + if (_values.TryGetValue(property, out var slot)) { - result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; + return new Diagnostics.AvaloniaPropertyValue( + property, + slot.Value.HasValue ? (object)slot.Value : AvaloniaProperty.UnsetValue, + slot.ValuePriority, + null); } - return result; + return null; } - public bool IsAnimating(AvaloniaProperty property) + void IValueSink.ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue) { - return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + _sink.ValueChanged(property, priority, oldValue, newValue); } - public bool IsSet(AvaloniaProperty property) + void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { - if (_propertyValues.TryGetValue(property, out var value)) + if (_values.TryGetValue(property, out var slot)) { - return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; + if (slot == entry) + { + _values.Remove(property); + } } - - return false; } - public void Revalidate(AvaloniaProperty property) + private void SetExisting( + object slot, + StyledPropertyBase property, + T value, + BindingPriority priority) { - if (_propertyValues.TryGetValue(property, out var value)) + if (slot is IPriorityValueEntry e) { - (value as PriorityValue)?.Revalidate(); + var priorityValue = new PriorityValue(_owner, property, this, e); + _values.SetValue(property, priorityValue); + priorityValue.SetValue(value, priority); } - } - - public void VerifyAccess() => _owner.VerifyAccess(); - - private PriorityValue CreatePriorityValue(AvaloniaProperty property) - { - var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); - Func validate2 = null; - - if (validate != null) + else if (slot is PriorityValue p) { - validate2 = v => validate(_owner, v); + p.SetValue(value, priority); + } + else if (slot is LocalValueEntry l) + { + if (priority == BindingPriority.LocalValue) + { + var old = l.Value; + l.SetValue(value); + _sink.ValueChanged(property, priority, old, value); + } + else + { + var priorityValue = new PriorityValue(_owner, property, this, l); + _values.SetValue(property, priorityValue); + } + } + else + { + throw new NotSupportedException("Unrecognised value store slot type."); } - - return new PriorityValue( - this, - property, - property.PropertyType, - validate2); } - private void Validate(AvaloniaProperty property, ref object value) + private IDisposable BindExisting( + object slot, + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) { - var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); + PriorityValue priorityValue; - if (validate != null && value != AvaloniaProperty.UnsetValue) + if (slot is IPriorityValueEntry e) { - value = validate(_owner, value); + priorityValue = new PriorityValue(_owner, property, this, e); } - } - - private DeferredSetter GetDeferredSetter(AvaloniaProperty property) - { - if (_deferredSetters.TryGetValue(property, out var deferredSetter)) + else if (slot is PriorityValue p) { - return (DeferredSetter)deferredSetter; + priorityValue = p; + } + else if (slot is LocalValueEntry l) + { + priorityValue = new PriorityValue(_owner, property, this, l); + } + else + { + throw new NotSupportedException("Unrecognised value store slot type."); } - var newDeferredSetter = new DeferredSetter(); - - _deferredSetters.AddValue(property, newDeferredSetter); - - return newDeferredSetter; - } - - public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) - { - return GetDeferredSetter(property); - } - - public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) - { - return GetDeferredSetter(property); + var binding = priorityValue.AddBinding(source, priority); + _values.SetValue(property, priorityValue); + binding.Start(); + return binding; } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index b65fd2a8b7f..86133d5fdb2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -202,20 +202,12 @@ public bool CanUserSortColumns AvaloniaProperty.Register( nameof(ColumnHeaderHeight), defaultValue: double.NaN, - validate: ValidateColumnHeaderHeight); + validate: IsValidColumnHeaderHeight); - private static double ValidateColumnHeaderHeight(DataGrid grid, double value) + private static bool IsValidColumnHeaderHeight(double value) { - if (value < DATAGRID_minimumColumnHeaderHeight) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_minimumColumnHeaderHeight); - } - if (value > DATAGRID_maxHeadersThickness) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_maxHeadersThickness); - } - - return value; + return double.IsNaN(value) || + (value >= DATAGRID_minimumColumnHeaderHeight && value <= DATAGRID_maxHeadersThickness); } /// @@ -273,15 +265,7 @@ public int FrozenColumnCount set { SetValue(FrozenColumnCountProperty, value); } } - private static int ValidateFrozenColumnCount(DataGrid grid, int value) - { - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); - } - - return value; - } + private static bool ValidateFrozenColumnCount(int value) => value >= 0; public static readonly StyledProperty GridLinesVisibilityProperty = AvaloniaProperty.Register(nameof(GridLinesVisibility)); @@ -396,29 +380,11 @@ public bool IsValid AvaloniaProperty.Register( nameof(MaxColumnWidth), defaultValue: DATAGRID_defaultMaxColumnWidth, - validate: ValidateMaxColumnWidth); + validate: IsValidColumnWidth); - private static double ValidateMaxColumnWidth(DataGrid grid, double value) + private static bool IsValidColumnWidth(double value) { - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MaxColumnWidth)); - } - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), 0); - } - if (grid.MinColumnWidth > value) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), nameof(MinColumnWidth)); - } - - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); - } - - return value; + return !double.IsNaN(value) && value > 0; } /// @@ -434,28 +400,11 @@ public double MaxColumnWidth AvaloniaProperty.Register( nameof(MinColumnWidth), defaultValue: DATAGRID_defaultMinColumnWidth, - validate: ValidateMinColumnWidth); + validate: IsValidMinColumnWidth); - private static double ValidateMinColumnWidth(DataGrid grid, double value) + private static bool IsValidMinColumnWidth(double value) { - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MinColumnWidth)); - } - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MinColumnWidth), 0); - } - if (double.IsPositiveInfinity(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(MinColumnWidth)); - } - if (grid.MaxColumnWidth < value) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(MinColumnWidth), nameof(MaxColumnWidth)); - } - - return value; + return !double.IsNaN(value) && !double.IsPositiveInfinity(value) && value >= 0; } /// @@ -483,19 +432,12 @@ public IBrush RowBackground AvaloniaProperty.Register( nameof(RowHeight), defaultValue: double.NaN, - validate: ValidateRowHeight); - private static double ValidateRowHeight(DataGrid grid, double value) + validate: IsValidRowHeight); + private static bool IsValidRowHeight(double value) { - if (value < DataGridRow.DATAGRIDROW_minimumHeight) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeight), 0); - } - if (value > DataGridRow.DATAGRIDROW_maximumHeight) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeight), DataGridRow.DATAGRIDROW_maximumHeight); - } - - return value; + return double.IsNaN(value) || + (value >= DataGridRow.DATAGRIDROW_minimumHeight && + value <= DataGridRow.DATAGRIDROW_maximumHeight); } /// @@ -511,19 +453,12 @@ public double RowHeight AvaloniaProperty.Register( nameof(RowHeaderWidth), defaultValue: double.NaN, - validate: ValidateRowHeaderWidth); - private static double ValidateRowHeaderWidth(DataGrid grid, double value) + validate: IsValidRowHeaderWidth); + private static bool IsValidRowHeaderWidth(double value) { - if (value < DATAGRID_minimumRowHeaderWidth) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_minimumRowHeaderWidth); - } - if (value > DATAGRID_maxHeadersThickness) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_maxHeadersThickness); - } - - return value; + return double.IsNaN(value) || + (value >= DATAGRID_minimumRowHeaderWidth && + value <= DATAGRID_maxHeadersThickness); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 7dafef9d8bb..69dfed761fa 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -68,25 +68,11 @@ public bool IsPropertyNameVisible AvaloniaProperty.Register( nameof(SublevelIndent), defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent, - validate: ValidateSublevelIndent); + validate: IsValidSublevelIndent); - private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value) + private static bool IsValidSublevelIndent(double value) { - // We don't need to revert to the old value if our input is bad because we never read this property value - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(SublevelIndent)); - } - else if (double.IsInfinity(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(SublevelIndent)); - } - else if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(SublevelIndent), 0); - } - - return value; + return !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0; } /// diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs index 060922238d8..9feca71cda7 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// public class DataGridFrozenGrid : Grid { - public static readonly AvaloniaProperty IsFrozenProperty = + public static readonly StyledProperty IsFrozenProperty = AvaloniaProperty.RegisterAttached("IsFrozen"); /// diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 64db832a815..6deddef0d08 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -378,7 +378,7 @@ public class AutoCompleteBox : TemplatedControl public static readonly StyledProperty MinimumPrefixLengthProperty = AvaloniaProperty.Register( nameof(MinimumPrefixLength), 1, - validate: ValidateMinimumPrefixLength); + validate: IsValidMinimumPrefixLength); /// /// Identifies the @@ -392,7 +392,7 @@ public class AutoCompleteBox : TemplatedControl AvaloniaProperty.Register( nameof(MinimumPopulateDelay), TimeSpan.Zero, - validate: ValidateMinimumPopulateDelay); + validate: IsValidMinimumPopulateDelay); /// /// Identifies the @@ -406,7 +406,7 @@ public class AutoCompleteBox : TemplatedControl AvaloniaProperty.Register( nameof(MaxDropDownHeight), double.PositiveInfinity, - validate: ValidateMaxDropDownHeight); + validate: IsValidMaxDropDownHeight); /// /// Identifies the @@ -495,7 +495,7 @@ public class AutoCompleteBox : TemplatedControl AvaloniaProperty.Register( nameof(FilterMode), defaultValue: AutoCompleteFilterMode.StartsWith, - validate: ValidateFilterMode); + validate: IsValidFilterMode); /// /// Identifies the @@ -546,26 +546,11 @@ public class AutoCompleteBox : TemplatedControl o => o.AsyncPopulator, (o, v) => o.AsyncPopulator = v); - private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value) - { - Contract.Requires(value >= -1); - - return value; - } - - private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value) - { - Contract.Requires(value.TotalMilliseconds >= 0.0); - - return value; - } + private static bool IsValidMinimumPrefixLength(int value) => value >= -1; - private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value) - { - Contract.Requires(value >= 0.0); + private static bool IsValidMinimumPopulateDelay(TimeSpan value) => value.TotalMilliseconds >= 0.0; - return value; - } + private static bool IsValidMaxDropDownHeight(double value) => value >= 0.0; private static bool IsValidFilterMode(AutoCompleteFilterMode mode) { @@ -590,12 +575,6 @@ private static bool IsValidFilterMode(AutoCompleteFilterMode mode) return false; } } - private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value) - { - Contract.Requires(IsValidFilterMode(value)); - - return value; - } /// /// Handle the change of the IsEnabled property. diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 78d02e200fa..2e115463ac8 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -306,18 +306,18 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } } - + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - base.UpdateDataValidation(property, status); + base.UpdateDataValidation(property, value); if (property == CommandProperty) { - if (status?.ErrorType == BindingErrorType.Error) + if (value.Type == BindingValueType.BindingError) { if (_commandCanExecute) { diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index beafab3edfd..94f8ad41a0b 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -352,7 +352,8 @@ public IBrush HeaderBackground public static readonly StyledProperty DisplayModeProperty = AvaloniaProperty.Register( nameof(DisplayMode), - validate: ValidateDisplayMode); + validate: IsValidDisplayMode); + /// /// Gets or sets a value indicating whether the calendar is displayed in /// months, years, or decades. @@ -417,17 +418,6 @@ private void OnDisplayModePropertyChanged(AvaloniaPropertyChangedEventArgs e) } OnDisplayModeChanged(new CalendarModeChangedEventArgs((CalendarMode)e.OldValue, mode)); } - private static CalendarMode ValidateDisplayMode(Calendar o, CalendarMode mode) - { - if(IsValidDisplayMode(mode)) - { - return mode; - } - else - { - throw new ArgumentOutOfRangeException(nameof(mode), "Invalid DisplayMode"); - } - } private static bool IsValidDisplayMode(CalendarMode mode) { return mode == CalendarMode.Month diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 841b73cd92c..b4d4fed9fc0 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -190,13 +190,13 @@ public class DatePicker : TemplatedControl AvaloniaProperty.Register( nameof(SelectedDateFormat), defaultValue: DatePickerFormat.Short, - validate: ValidateSelectedDateFormat); + validate: IsValidSelectedDateFormat); public static readonly StyledProperty CustomDateFormatStringProperty = AvaloniaProperty.Register( nameof(CustomDateFormatString), defaultValue: "d", - validate: ValidateDateFormatString); + validate: IsValidDateFormatString); public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect( @@ -512,11 +512,17 @@ protected override void OnTemplateApplied(TemplateAppliedEventArgs e) base.OnTemplateApplied(e); } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) { + base.OnPropertyChanged(property, oldValue, newValue, priority); + if (property == SelectedDateProperty) { - DataValidationErrors.SetError(this, status.Error); + DataValidationErrors.SetError(this, newValue.Error); } } @@ -1140,27 +1146,9 @@ private static bool IsValidSelectedDateFormat(DatePickerFormat value) || value == DatePickerFormat.Short || value == DatePickerFormat.Custom; } - private static DatePickerFormat ValidateSelectedDateFormat(DatePicker dp, DatePickerFormat format) + private static bool IsValidDateFormatString(string formatString) { - if(IsValidSelectedDateFormat(format)) - { - return format; - } - else - { - throw new ArgumentOutOfRangeException(nameof(format), "DatePickerFormat value is not valid."); - } - } - private static string ValidateDateFormatString(DatePicker dp, string formatString) - { - if(string.IsNullOrWhiteSpace(formatString)) - { - throw new ArgumentException("DateFormatString value is not valid.", nameof(formatString)); - } - else - { - return formatString; - } + return !string.IsNullOrWhiteSpace(formatString); } private static DateTime DiscardDayTime(DateTime d) { diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index e4ae7774534..38ebbe5bf92 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -356,9 +356,13 @@ private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaP /// b) contains only letters, digits and underscore ('_'). /// c) does not start with a digit. /// - private static string SharedSizeGroupPropertyValueValid(Control _, string value) + private static bool SharedSizeGroupPropertyValueValid(string value) { - Contract.Requires(value != null); + // null is default value + if (value == null) + { + return true; + } string id = (string)value; @@ -380,11 +384,11 @@ private static string SharedSizeGroupPropertyValueValid(Control _, string value) if (i == id.Length) { - return value; + return true; } } - throw new ArgumentException("Invalid SharedSizeGroup string."); + return false; } /// diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 23c1cd47946..1781067abbe 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -2741,11 +2741,7 @@ private enum Flags AvaloniaProperty.RegisterAttached( "Column", defaultValue: 0, - validate: (_, v) => - { - if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Column value."); - }); + validate: v => v >= 0); /// /// Row property. This is an attached property. @@ -2762,11 +2758,7 @@ private enum Flags AvaloniaProperty.RegisterAttached( "Row", defaultValue: 0, - validate: (_, v) => - { - if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Row value."); - }); + validate: v => v >= 0); /// /// ColumnSpan property. This is an attached property. @@ -2782,11 +2774,7 @@ private enum Flags AvaloniaProperty.RegisterAttached( "ColumnSpan", defaultValue: 1, - validate: (_, v) => - { - if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.ColumnSpan value."); - }); + validate: v => v >= 0); /// /// RowSpan property. This is an attached property. @@ -2802,11 +2790,7 @@ private enum Flags AvaloniaProperty.RegisterAttached( "RowSpan", defaultValue: 1, - validate: (_, v) => - { - if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.RowSpan value."); - }); + validate: v => v >= 0); /// /// IsSharedSizeScope property marks scoping element for shared size. diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index a2fefa05488..a2d53f4f067 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -23,37 +23,37 @@ public class GridSplitter : Thumb /// /// Defines the property. /// - public static readonly AvaloniaProperty ResizeDirectionProperty = + public static readonly StyledProperty ResizeDirectionProperty = AvaloniaProperty.Register(nameof(ResizeDirection)); /// /// Defines the property. /// - public static readonly AvaloniaProperty ResizeBehaviorProperty = + public static readonly StyledProperty ResizeBehaviorProperty = AvaloniaProperty.Register(nameof(ResizeBehavior)); /// /// Defines the property. /// - public static readonly AvaloniaProperty ShowsPreviewProperty = + public static readonly StyledProperty ShowsPreviewProperty = AvaloniaProperty.Register(nameof(ShowsPreview)); /// /// Defines the property. /// - public static readonly AvaloniaProperty KeyboardIncrementProperty = + public static readonly StyledProperty KeyboardIncrementProperty = AvaloniaProperty.Register(nameof(KeyboardIncrement), 10d); /// /// Defines the property. /// - public static readonly AvaloniaProperty DragIncrementProperty = + public static readonly StyledProperty DragIncrementProperty = AvaloniaProperty.Register(nameof(DragIncrement), 1d); /// /// Defines the property. /// - public static readonly AvaloniaProperty> PreviewContentProperty = + public static readonly StyledProperty> PreviewContentProperty = AvaloniaProperty.Register>(nameof(PreviewContent)); private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast); diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index db67a241595..a3bb6546293 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -17,10 +17,10 @@ namespace Avalonia.Controls /// public class LayoutTransformControl : Decorator { - public static readonly AvaloniaProperty LayoutTransformProperty = + public static readonly StyledProperty LayoutTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); - public static readonly AvaloniaProperty UseRenderTransformProperty = + public static readonly StyledProperty UseRenderTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); static LayoutTransformControl() diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 3ba0007f6ba..e0baa5e679d 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -26,8 +26,8 @@ public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable /// public static readonly DirectProperty CommandProperty = Button.CommandProperty.AddOwner( - menuItem => menuItem.Command, - (menuItem, command) => menuItem.Command = command, + menuItem => menuItem.Command, + (menuItem, command) => menuItem.Command = command, enableDataValidation: true); /// @@ -394,12 +394,12 @@ protected override void OnTemplateApplied(TemplateAppliedEventArgs e) } } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - base.UpdateDataValidation(property, status); + base.UpdateDataValidation(property, value); if (property == CommandProperty) { - if (status?.ErrorType == BindingErrorType.Error) + if (value.Type == BindingValueType.BindingError) { if (_commandCanExecute) { diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 5d3a4526cc8..776e9d2171f 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -52,13 +52,13 @@ static void SetIsNativeMenuExported(TopLevel tl, bool value) } public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("Menu", validate: + = AvaloniaProperty.RegisterAttached("Menu"/*, validate: (o, v) => { if(!(o is Application || o is TopLevel)) throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType()); return v; - }); + }*/); public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 7f69afaeeb6..9e9eeec7c53 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -118,7 +118,7 @@ public static void SetCloseOnClick(Button obj, bool value) /// Defines the CloseOnClick property. /// public static readonly AvaloniaProperty CloseOnClickProperty = - AvaloniaProperty.RegisterAttached("CloseOnClick", typeof(NotificationCard), validate: CloseOnClickChanged); + AvaloniaProperty.RegisterAttached("CloseOnClick", typeof(NotificationCard)/*, validate: CloseOnClickChanged*/); private static bool CloseOnClickChanged(Button button, bool value) { diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 6d450a01559..cbb5b667e73 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -58,7 +58,7 @@ public class NumericUpDown : TemplatedControl /// Defines the property. /// public static readonly StyledProperty IncrementProperty = - AvaloniaProperty.Register(nameof(Increment), 1.0d, validate: OnCoerceIncrement); + AvaloniaProperty.Register(nameof(Increment), 1.0d, coerce: OnCoerceIncrement); /// /// Defines the property. @@ -70,13 +70,13 @@ public class NumericUpDown : TemplatedControl /// Defines the property. /// public static readonly StyledProperty MaximumProperty = - AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum); + AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum); /// /// Defines the property. /// public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum); + AvaloniaProperty.Register(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum); /// /// Defines the property. @@ -738,19 +738,34 @@ private void SetValueInternal(double value) } } - private static double OnCoerceMaximum(NumericUpDown upDown, double value) + private static double OnCoerceMaximum(IAvaloniaObject instance, double value) { - return upDown.OnCoerceMaximum(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceMaximum(value); + } + + return value; } - private static double OnCoerceMinimum(NumericUpDown upDown, double value) + private static double OnCoerceMinimum(IAvaloniaObject instance, double value) { - return upDown.OnCoerceMinimum(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceMinimum(value); + } + + return value; } - private static double OnCoerceIncrement(NumericUpDown upDown, double value) + private static double OnCoerceIncrement(IAvaloniaObject instance, double value) { - return upDown.OnCoerceIncrement(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceIncrement(value); + } + + return value; } private void TextBoxOnTextChanged() diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 5e1a844720a..9b1215c9ae5 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -28,7 +28,7 @@ public class ContentPresenter : Control, IContentPresenter /// /// Defines the property. /// - public static readonly AvaloniaProperty BorderBrushProperty = + public static readonly StyledProperty BorderBrushProperty = Border.BorderBrushProperty.AddOwner(); /// diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 9251ca273f3..4aaff94e447 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -73,7 +73,7 @@ public ScrollBar() this.GetObservable(ViewportSizeProperty).Select(_ => Unit.Default), this.GetObservable(VisibilityProperty).Select(_ => Unit.Default)) .Select(_ => CalculateIsVisible()); - Bind(IsVisibleProperty, isVisible, BindingPriority.Style); + this.Bind(IsVisibleProperty, isVisible, BindingPriority.Style); } /// diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index cbac1d6c1bd..086599d0bb9 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; @@ -21,7 +22,7 @@ public class ItemsRepeater : Panel /// /// Defines the property. /// - public static readonly AvaloniaProperty HorizontalCacheLengthProperty = + public static readonly StyledProperty HorizontalCacheLengthProperty = AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); /// @@ -39,16 +40,16 @@ public class ItemsRepeater : Panel /// /// Defines the property. /// - public static readonly AvaloniaProperty LayoutProperty = + public static readonly StyledProperty LayoutProperty = AvaloniaProperty.Register(nameof(Layout), new StackLayout()); /// /// Defines the property. /// - public static readonly AvaloniaProperty VerticalCacheLengthProperty = + public static readonly StyledProperty VerticalCacheLengthProperty = AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); - private static readonly AttachedProperty VirtualizationInfoProperty = + private static readonly StyledProperty VirtualizationInfoProperty = AvaloniaProperty.RegisterAttached("VirtualizationInfo"); internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); @@ -374,41 +375,37 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e _viewportManager.ResetScrollers(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { - var property = args.Property; - if (property == ItemsProperty) { - var newValue = (IEnumerable)args.NewValue; - var newDataSource = newValue as ItemsSourceView; - if (newValue != null && newDataSource == null) + var newEnumerable = newValue.GetValueOrDefault(); + var newDataSource = newEnumerable as ItemsSourceView; + if (newEnumerable != null && newDataSource == null) { - newDataSource = new ItemsSourceView(newValue); + newDataSource = new ItemsSourceView(newEnumerable); } OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); } else if (property == ItemTemplateProperty) { - OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); + OnItemTemplateChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); } else if (property == LayoutProperty) { - OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); + OnLayoutChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); } else if (property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = (double)args.NewValue; + _viewportManager.HorizontalCacheLength = newValue.GetValueOrDefault(); } else if (property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = (double)args.NewValue; - } - else - { - base.OnPropertyChanged(args); + _viewportManager.VerticalCacheLength = newValue.GetValueOrDefault(); } + + base.OnPropertyChanged(property, oldValue, newValue, priority); } internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index cdf50109205..4fae867dbd5 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -161,8 +161,6 @@ public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider /// static ScrollViewer() { - AffectsValidation(ExtentProperty, OffsetProperty); - AffectsValidation(ViewportProperty, OffsetProperty); HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index fbac2e02ec1..b561a3423fc 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -63,7 +63,7 @@ public class TextBox : TemplatedControl, UndoRedoHelper.I AvaloniaProperty.Register(nameof(MaxLength), defaultValue: 0); public static readonly DirectProperty TextProperty = - TextBlock.TextProperty.AddOwner( + TextBlock.TextProperty.AddOwnerWithDataValidation( o => o.Text, (o, v) => o.Text = v, defaultBindingMode: BindingMode.TwoWay, @@ -133,7 +133,7 @@ public TextBox() return ScrollBarVisibility.Hidden; } }); - Bind( + this.Bind( ScrollViewer.HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility, BindingPriority.Style); @@ -702,11 +702,11 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { if (property == TextProperty) { - DataValidationErrors.SetError(this, status.Error); + DataValidationErrors.SetError(this, value.Error); } } diff --git a/src/Avalonia.Input/DragDrop.cs b/src/Avalonia.Input/DragDrop.cs index d39659cee30..723d5779640 100644 --- a/src/Avalonia.Input/DragDrop.cs +++ b/src/Avalonia.Input/DragDrop.cs @@ -23,7 +23,7 @@ public static class DragDrop /// public static readonly RoutedEvent DropEvent = RoutedEvent.Register("Drop", RoutingStrategies.Bubble, typeof(DragDrop)); - public static readonly AvaloniaProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); + public static readonly AttachedProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); /// /// Gets a value indicating whether the given element can be used as the target of a drag-and-drop operation. diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 3c3729272c0..e8ad49e9b9e 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Data; namespace Avalonia.Layout { @@ -295,11 +296,11 @@ protected internal override void OnItemsChangedCore(VirtualizingLayoutContext co InvalidateLayout(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { - if (e.Property == OrientationProperty) + if (property == OrientationProperty) { - var orientation = (Orientation)e.NewValue; + var orientation = newValue.GetValueOrDefault(); //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 11a521ed1e3..54c3ccbb90b 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Data; namespace Avalonia.Layout { @@ -462,44 +463,45 @@ protected internal override void OnItemsChangedCore(VirtualizingLayoutContext co gridState.ClearElementOnDataSourceChange(context, args); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { - if (args.Property == OrientationProperty) + if (property == OrientationProperty) { - var orientation = (Orientation)args.NewValue; + var orientation = newValue.GetValueOrDefault(); //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //i.e. the properties are the inverse of each other. var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; _orientation.ScrollOrientation = scrollOrientation; } - else if (args.Property == MinColumnSpacingProperty) + else if (property == MinColumnSpacingProperty) { - _minColumnSpacing = (double)args.NewValue; + _minColumnSpacing = newValue.GetValueOrDefault(); } - else if (args.Property == MinRowSpacingProperty) + else if (property == MinRowSpacingProperty) { - _minRowSpacing = (double)args.NewValue; + _minRowSpacing = newValue.GetValueOrDefault(); } - else if (args.Property == ItemsJustificationProperty) + else if (property == ItemsJustificationProperty) { - _itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue; + _itemsJustification = newValue.GetValueOrDefault(); + ; } - else if (args.Property == ItemsStretchProperty) + else if (property == ItemsStretchProperty) { - _itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue; + _itemsStretch = newValue.GetValueOrDefault(); } - else if (args.Property == MinItemWidthProperty) + else if (property == MinItemWidthProperty) { - _minItemWidth = (double)args.NewValue; + _minItemWidth = newValue.GetValueOrDefault(); } - else if (args.Property == MinItemHeightProperty) + else if (property == MinItemHeightProperty) { - _minItemHeight = (double)args.NewValue; + _minItemHeight = newValue.GetValueOrDefault(); } - else if (args.Property == MaximumRowsOrColumnsProperty) + else if (property == MaximumRowsOrColumnsProperty) { - _maximumRowsOrColumns = (int)args.NewValue; + _maximumRowsOrColumns = newValue.GetValueOrDefault(); } InvalidateLayout(); diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 010acc3ae02..3a39beae94e 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class { - public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel>(nameof(ViewModel)); /// diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index f0f115afbcd..10ae610345d 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveWindow : Window, IViewFor where TViewModel : class { - public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel>(nameof(ViewModel)); /// @@ -41,4 +41,4 @@ object IViewFor.ViewModel set => ViewModel = (TViewModel)value; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index ac5db32c144..e111b15c75f 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -58,7 +58,7 @@ public class RoutedViewHost : TransitioningContentControl, IActivatableView, IEn /// /// for the property. /// - public static readonly AvaloniaProperty RouterProperty = + public static readonly StyledProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); /// @@ -118,4 +118,4 @@ private void NavigateToViewModel(object viewModel) Content = viewInstance; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 1bec5fc3654..85768a39c35 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -16,14 +16,14 @@ public class TransitioningContentControl : ContentControl, IStyleable /// /// for the property. /// - public static readonly AvaloniaProperty PageTransitionProperty = + public static readonly StyledProperty PageTransitionProperty = AvaloniaProperty.Register(nameof(PageTransition), new CrossFade(TimeSpan.FromSeconds(0.5))); /// /// for the property. /// - public static readonly AvaloniaProperty DefaultContentProperty = + public static readonly StyledProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); /// @@ -72,4 +72,4 @@ private async void UpdateContentWithTransition(object content) await PageTransition.Start(null, this, true); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 33962b84997..83e37d826e3 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -471,7 +471,11 @@ void ISetLogicalParent.SetParent(ILogical parent) OnAttachedToLogicalTreeCore(e); } - RaisePropertyChanged(ParentProperty, old, Parent, BindingPriority.LocalValue); + RaisePropertyChanged( + ParentProperty, + new Optional(old), + new BindingValue(Parent), + BindingPriority.LocalValue); } } diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index 7784c73736f..1e813edc13e 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -14,13 +14,13 @@ public class DashStyle : Animatable, IDashStyle, IAffectsRender /// /// Defines the property. /// - public static readonly AvaloniaProperty> DashesProperty = + public static readonly StyledProperty> DashesProperty = AvaloniaProperty.Register>(nameof(Dashes)); /// /// Defines the property. /// - public static readonly AvaloniaProperty OffsetProperty = + public static readonly StyledProperty OffsetProperty = AvaloniaProperty.Register(nameof(Offset)); private static ImmutableDashStyle s_dash; diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs index d6ab004dd7c..57939bab24c 100644 --- a/src/Avalonia.Visuals/Media/DrawingImage.cs +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Visuals.Media.Imaging; @@ -62,11 +63,15 @@ void IImage.Draw( } /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) { - base.OnPropertyChanged(e); + base.OnPropertyChanged(property, oldValue, newValue, priority); - if (e.Property == DrawingProperty) + if (property == DrawingProperty) { RaiseInvalidated(EventArgs.Empty); } diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs index 5ed16ca9572..0fdc40c85c9 100644 --- a/src/Avalonia.Visuals/Media/PolylineGeometry.cs +++ b/src/Avalonia.Visuals/Media/PolylineGeometry.cs @@ -23,7 +23,7 @@ public class PolylineGeometry : Geometry /// /// Defines the property. /// - public static readonly AvaloniaProperty IsFilledProperty = + public static readonly StyledProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); private Points _points; diff --git a/src/Avalonia.Visuals/Media/TransformGroup.cs b/src/Avalonia.Visuals/Media/TransformGroup.cs index 3a47f400451..886a6479dd7 100644 --- a/src/Avalonia.Visuals/Media/TransformGroup.cs +++ b/src/Avalonia.Visuals/Media/TransformGroup.cs @@ -11,7 +11,7 @@ public class TransformGroup : Transform /// /// Defines the property. /// - public static readonly AvaloniaProperty ChildrenProperty = + public static readonly StyledProperty ChildrenProperty = AvaloniaProperty.Register(nameof(Children)); public TransformGroup() diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 465b3a65beb..ef7a254f862 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -447,7 +447,11 @@ protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) /// The new visual parent. protected virtual void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { - RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); + RaisePropertyChanged( + VisualParentProperty, + new Optional(oldParent), + new BindingValue(newParent), + BindingPriority.LocalValue); } protected override sealed void LogBindingError(AvaloniaProperty property, Exception e) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs index 4e033be3fbe..e523c312ac8 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using Xunit; namespace Avalonia.Base.UnitTests @@ -16,31 +15,12 @@ public void AddOwnered_Property_Retains_Default_Value() Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); } - [Fact] - public void AddOwnered_Property_Does_Not_Retain_Validation() - { - var target = new Class2(); - - target.SetValue(Class2.FooProperty, "throw"); - } - private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register( "Foo", - "foodefault", - validate: ValidateFoo); - - private static string ValidateFoo(AvaloniaObject arg1, string arg2) - { - if (arg2 == "throw") - { - throw new IndexOutOfRangeException(); - } - - return arg2; - } + "foodefault"); } private class Class2 : AvaloniaObject diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs index 02600f5e00e..44e2976e038 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs @@ -16,14 +16,6 @@ public void AddOwnered_Property_Retains_Default_Value() Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); } - [Fact] - public void AddOwnered_Property_Retains_Validation() - { - var target = new Class2(); - - Assert.Throws(() => target.SetValue(Class2.FooProperty, "throw")); - } - [Fact] public void AvaloniaProperty_Initialized_Is_Called_For_Attached_Property() { @@ -46,18 +38,7 @@ private class Class1 : Base public static readonly AttachedProperty FooProperty = AvaloniaProperty.RegisterAttached( "Foo", - "foodefault", - validate: ValidateFoo); - - private static string ValidateFoo(AvaloniaObject arg1, string arg2) - { - if (arg2 == "throw") - { - throw new IndexOutOfRangeException(); - } - - return arg2; - } + "foodefault"); } private class Class2 : Base diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 23984a7c8d6..4c00d2a1ea6 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -3,18 +3,15 @@ using System; using System.ComponentModel; -using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Logging; -using Avalonia.Markup.Data; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; -using Avalonia.Diagnostics; using Microsoft.Reactive.Testing; using Moq; using Xunit; @@ -26,13 +23,189 @@ public class AvaloniaObjectTests_Binding [Fact] public void Bind_Sets_Current_Value() { - Class1 target = new Class1(); - Class1 source = new Class1(); + var target = new Class1(); + var source = new Class1(); + var property = Class1.FooProperty; - source.SetValue(Class1.FooProperty, "initial"); - target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); + source.SetValue(property, "initial"); + target.Bind(property, source.GetObservable(property)); - Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + Assert.Equal("initial", target.GetValue(property)); + } + + [Fact] + public void Bind_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new Subject>(); + bool raised = false; + + target.PropertyChanged += (s, e) => + raised = e.Property == Class1.FooProperty && + (string)e.OldValue == "foodefault" && + (string)e.NewValue == "newvalue" && + e.Priority == BindingPriority.LocalValue; + + target.Bind(Class1.FooProperty, source); + source.OnNext("newvalue"); + + Assert.True(raised); + } + + [Fact] + public void PropertyChanged_Not_Raised_When_Value_Unchanged() + { + var target = new Class1(); + var source = new Subject>(); + var raised = 0; + + target.PropertyChanged += (s, e) => ++raised; + target.Bind(Class1.FooProperty, source); + source.OnNext("newvalue"); + source.OnNext("newvalue"); + + Assert.Equal(1, raised); + } + + [Fact] + public void Setting_LocalValue_Overrides_Binding_Until_Binding_Produces_Next_Value() + { + var target = new Class1(); + var source = new Subject(); + var property = Class1.FooProperty; + + target.Bind(property, source); + source.OnNext("foo"); + Assert.Equal("foo", target.GetValue(property)); + + target.SetValue(property, "bar"); + Assert.Equal("bar", target.GetValue(property)); + + source.OnNext("baz"); + Assert.Equal("baz", target.GetValue(property)); + } + + [Fact] + public void Completing_LocalValue_Binding_Reverts_To_Default_Value_Even_When_Local_Value_Set_Earlier() + { + var target = new Class1(); + var source = new Subject(); + var property = Class1.FooProperty; + + target.Bind(property, source); + source.OnNext("foo"); + target.SetValue(property, "bar"); + source.OnNext("baz"); + source.OnCompleted(); + + Assert.Equal("foodefault", target.GetValue(property)); + } + + [Fact] + public void Completing_LocalValue_Binding_Should_Not_Revert_To_Set_LocalValue() + { + var target = new Class1(); + var source = new BehaviorSubject("bar"); + + target.SetValue(Class1.FooProperty, "foo"); + var sub = target.Bind(Class1.FooProperty, source); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + + sub.Dispose(); + + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Completing_Animation_Binding_Reverts_To_Set_LocalValue() + { + var target = new Class1(); + var source = new Subject(); + var property = Class1.FooProperty; + + target.SetValue(property, "foo"); + target.Bind(property, source, BindingPriority.Animation); + source.OnNext("bar"); + source.OnCompleted(); + + Assert.Equal("foo", target.GetValue(property)); + } + + [Fact] + public void Setting_Style_Value_Overrides_Binding_Permanently() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source, BindingPriority.Style); + source.OnNext("foo"); + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + + target.SetValue(Class1.FooProperty, "bar", BindingPriority.Style); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + + source.OnNext("baz"); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Second_LocalValue_Binding_Overrides_First() + { + var property = Class1.FooProperty; + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); + + target.Bind(property, source1, BindingPriority.LocalValue); + target.Bind(property, source2, BindingPriority.LocalValue); + + source1.OnNext("foo"); + Assert.Equal("foo", target.GetValue(property)); + + source2.OnNext("bar"); + Assert.Equal("bar", target.GetValue(property)); + + source1.OnNext("baz"); + Assert.Equal("bar", target.GetValue(property)); + } + + [Fact] + public void Completing_Second_LocalValue_Binding_Reverts_To_First() + { + var property = Class1.FooProperty; + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); + + target.Bind(property, source1, BindingPriority.LocalValue); + target.Bind(property, source2, BindingPriority.LocalValue); + + source1.OnNext("foo"); + source2.OnNext("bar"); + source1.OnNext("baz"); + source2.OnCompleted(); + + Assert.Equal("baz", target.GetValue(property)); + } + + [Fact] + public void Completing_StyleTrigger_Binding_Reverts_To_StyleBinding() + { + var property = Class1.FooProperty; + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); + + target.Bind(property, source1, BindingPriority.Style); + target.Bind(property, source2, BindingPriority.StyleTrigger); + + source1.OnNext("foo"); + source2.OnNext("bar"); + source2.OnCompleted(); + source1.OnNext("baz"); + + Assert.Equal("baz", target.GetValue(property)); } [Fact] @@ -126,7 +299,7 @@ public void Bind_Ignores_Invalid_Value_Type() public void Observable_Is_Unsubscribed_When_Subscription_Disposed() { var scheduler = new TestScheduler(); - var source = scheduler.CreateColdObservable(); + var source = scheduler.CreateColdObservable(); var target = new Class1(); var subscription = target.Bind(Class1.FooProperty, source); @@ -191,13 +364,13 @@ public void Two_Way_Binding_With_Priority_Works() obj2.SetValue(Class1.FooProperty, "second", BindingPriority.Style); - Assert.Equal("second", obj1.GetValue(Class1.FooProperty)); + Assert.Equal("first", obj1.GetValue(Class1.FooProperty)); Assert.Equal("second", obj2.GetValue(Class1.FooProperty)); obj1.SetValue(Class1.FooProperty, "third", BindingPriority.Style); Assert.Equal("third", obj1.GetValue(Class1.FooProperty)); - Assert.Equal("third", obj2.GetValue(Class1.FooProperty)); + Assert.Equal("second", obj2.GetValue(Class1.FooProperty)); } [Fact] @@ -302,41 +475,62 @@ public void this_Operator_Binds_One_Time() } [Fact] - public void BindingError_Does_Not_Cause_Target_Update() + public void Binding_Error_Reverts_To_Default_Value() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); - target.Bind(Class1.QuxProperty, source); - source.OnNext(6.7); - source.OnNext(new BindingNotification( - new InvalidOperationException("Foo"), - BindingErrorType.Error)); + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"))); - Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } [Fact] - public void BindingNotification_With_FallbackValue_Causes_Target_Update() + public void Binding_Error_With_FallbackValue_Causes_Target_Update() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); - target.Bind(Class1.QuxProperty, source); - source.OnNext(6.7); - source.OnNext(new BindingNotification( - new InvalidOperationException("Foo"), - BindingErrorType.Error, - 8.9)); + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"), "bar")); - Assert.Equal(8.9, target.GetValue(Class1.QuxProperty)); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void DataValidationError_Does_Not_Cause_Target_Update() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.DataValidationError(new InvalidOperationException("Foo"))); + + Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void DataValidationError_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.DataValidationError(new InvalidOperationException("Foo"), "bar")); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); } [Fact] public void Bind_Logs_Binding_Error() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); var called = false; var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}"; @@ -354,9 +548,7 @@ public void Bind_Logs_Binding_Error() { target.Bind(Class1.QuxProperty, source); source.OnNext(6.7); - source.OnNext(new BindingNotification( - new InvalidOperationException("Foo"), - BindingErrorType.Error)); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"))); Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); Assert.True(called); @@ -367,7 +559,7 @@ public void Bind_Logs_Binding_Error() public async Task Bind_With_Scheduler_Executes_On_Scheduler() { var target = new Class1(); - var source = new Subject(); + var source = new Subject(); var currentThreadId = Thread.CurrentThread.ManagedThreadId; var threadingInterfaceMock = new Mock(); @@ -426,13 +618,13 @@ public void IsAnimating_On_Property_With_No_Value_Returns_False() } [Fact] - public void IsAnimating_On_Property_With_Animation_Value_Returns_False() + public void IsAnimating_On_Property_With_Animation_Value_Returns_True() { var target = new Class1(); target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation); - Assert.False(target.IsAnimating(Class1.FooProperty)); + Assert.True(target.IsAnimating(Class1.FooProperty)); } [Fact] @@ -457,6 +649,30 @@ public void IsAnimating_On_Property_With_Animation_Binding_Returns_True() Assert.True(target.IsAnimating(Class1.FooProperty)); } + [Fact] + public void IsAnimating_On_Property_With_Local_Value_And_Animation_Binding_Returns_True() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.SetValue(Class1.FooProperty, "bar"); + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.True(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_Returns_True_When_Animated_Value_Is_Same_As_Local_Value() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.SetValue(Class1.FooProperty, "foo"); + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.True(target.IsAnimating(Class1.FooProperty)); + } + [Fact] public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs new file mode 100644 index 00000000000..3efb926ac33 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -0,0 +1,155 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Subjects; +using Avalonia.Data; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_Coercion + { + [Fact] + public void Coerces_Set_Value() + { + var target = new Class1(); + + target.Foo = 150; + + Assert.Equal(100, target.Foo); + } + + [Fact] + public void Coerces_Set_Value_Attached() + { + var target = new Class1(); + + target.SetValue(Class1.AttachedProperty, 150); + + Assert.Equal(100, target.GetValue(Class1.AttachedProperty)); + } + + [Fact] + public void Coerces_Bound_Value() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext(150); + + Assert.Equal(100, target.Foo); + } + + [Fact] + public void CoerceValue_Updates_Value() + { + var target = new Class1 { Foo = 99 }; + + Assert.Equal(99, target.Foo); + + target.MaxFoo = 50; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(50, target.Foo); + } + + [Fact] + public void Coerced_Value_Can_Be_Restored_If_Limit_Changed() + { + var target = new Class1(); + + target.Foo = 150; + Assert.Equal(100, target.Foo); + + target.MaxFoo = 200; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(150, target.Foo); + } + + [Fact] + public void Coerced_Value_Can_Be_Restored_From_Previously_Active_Binding() + { + var target = new Class1(); + var source1 = new Subject>(); + var source2 = new Subject>(); + + target.Bind(Class1.FooProperty, source1); + source1.OnNext(150); + + target.Bind(Class1.FooProperty, source2); + source2.OnNext(160); + + Assert.Equal(100, target.Foo); + + target.MaxFoo = 200; + source2.OnCompleted(); + + Assert.Equal(150, target.Foo); + } + + [Fact] + public void Coercion_Can_Be_Overridden() + { + var target = new Class2(); + + target.Foo = 150; + + Assert.Equal(-150, target.Foo); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Qux", + defaultValue: 11, + coerce: CoerceFoo); + + public static readonly AttachedProperty AttachedProperty = + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + coerce: CoerceFoo); + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public int MaxFoo { get; set; } = 100; + + public static int CoerceFoo(IAvaloniaObject instance, int value) + { + return Math.Min(((Class1)instance).MaxFoo, value); + } + } + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + Class1.FooProperty.AddOwner(); + + static Class2() + { + FooProperty.OverrideMetadata( + new StyledPropertyMetadata( + coerce: CoerceFoo)); + } + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public static int CoerceFoo(IAvaloniaObject instance, int value) + { + return -value; + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index 428f8789453..e8cc71c7230 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -11,59 +11,31 @@ namespace Avalonia.Base.UnitTests public class AvaloniaObjectTests_DataValidation { [Fact] - public void Setting_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() + public void Binding_Non_Validated_Styled_Property_Does_Not_Call_UpdateDataValidation() { var target = new Class1(); + var source = new Subject>(); - target.SetValue(Class1.NonValidatedDirectProperty, 6); + target.Bind(Class1.NonValidatedProperty, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.DataValidationError(new Exception())); + source.OnNext(6); Assert.Empty(target.Notifications); } [Fact] - public void Setting_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation() + public void Binding_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation() { var target = new Class1(); + var source = new Subject>(); - target.SetValue(Class1.NonValidatedDirectProperty, 6); - - Assert.Empty(target.Notifications); - } - - [Fact] - public void Setting_Validated_Direct_Property_Calls_UpdateDataValidation() - { - var target = new Class1(); - - target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6)); - target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); - target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7)); - - Assert.Equal( - new[] - { - new BindingNotification(6), - new BindingNotification(new Exception(), BindingErrorType.Error), - new BindingNotification(new Exception(), BindingErrorType.DataValidationError), - new BindingNotification(7), - }, - target.Notifications.AsEnumerable()); - } - - [Fact] - public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() - { - var source = new Subject(); - var target = new Class1 - { - [!Class1.NonValidatedProperty] = source.ToBinding(), - }; - - source.OnNext(new BindingNotification(6)); - source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error)); - source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - source.OnNext(new BindingNotification(7)); + target.Bind(Class1.NonValidatedDirectProperty, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.DataValidationError(new Exception())); + source.OnNext(6); Assert.Empty(target.Notifications); } @@ -71,26 +43,23 @@ public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() [Fact] public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation() { - var source = new Subject(); - var target = new Class1 - { - [!Class1.ValidatedDirectIntProperty] = source.ToBinding(), - }; - - source.OnNext(new BindingNotification(6)); - source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error)); - source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - source.OnNext(new BindingNotification(7)); - - Assert.Equal( - new[] - { - new BindingNotification(6), - new BindingNotification(new Exception(), BindingErrorType.Error), - new BindingNotification(new Exception(), BindingErrorType.DataValidationError), - new BindingNotification(7), - }, - target.Notifications.AsEnumerable()); + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.ValidatedDirectIntProperty, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.DataValidationError(new Exception())); + source.OnNext(7); + + var result = target.Notifications.Cast>().ToList(); + Assert.Equal(4, result.Count); + Assert.Equal(BindingValueType.Value, result[0].Type); + Assert.Equal(6, result[0].Value); + Assert.Equal(BindingValueType.BindingError, result[1].Type); + Assert.Equal(BindingValueType.DataValidationError, result[2].Type); + Assert.Equal(BindingValueType.Value, result[3].Type); + Assert.Equal(7, result[3].Value); } [Fact] @@ -171,11 +140,13 @@ public string ValidatedDirectString set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); } } - public IList Notifications { get; } = new List(); + public IList Notifications { get; } = new List(); - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification notification) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValue value) { - Notifications.Add(notification); + Notifications.Add(value); } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 980cbfaaf8e..4110c3771f7 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -7,12 +7,9 @@ using System.Reactive.Subjects; using System.Threading; using System.Threading.Tasks; -using Avalonia; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Platform; -using Avalonia.Threading; -using Avalonia.Markup.Data; using Avalonia.UnitTests; using Moq; using Xunit; @@ -22,7 +19,7 @@ namespace Avalonia.Base.UnitTests public class AvaloniaObjectTests_Direct { [Fact] - public void GetValue_Gets_Value() + public void GetValue_Gets_Default_Value() { var target = new Class1(); @@ -109,6 +106,62 @@ public void SetValue_Raises_Changed() Assert.True(raised); } + [Fact] + public void Setting_Object_Property_To_UnsetValue_Reverts_To_Default_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FrankProperty, "newvalue"); + target.SetValue(Class1.FrankProperty, AvaloniaProperty.UnsetValue); + + Assert.Equal("Kups", target.GetValue(Class1.FrankProperty)); + } + + [Fact] + public void Setting_Object_Property_To_DoNothing_Does_Nothing() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FrankProperty, "newvalue"); + target.SetValue(Class1.FrankProperty, BindingOperations.DoNothing); + + Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty)); + } + + [Fact] + public void Bind_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new Subject>(); + bool raised = false; + + target.PropertyChanged += (s, e) => + raised = e.Property == Class1.FooProperty && + (string)e.OldValue == "initial" && + (string)e.NewValue == "newvalue" && + e.Priority == BindingPriority.LocalValue; + + target.Bind(Class1.FooProperty, source); + source.OnNext("newvalue"); + + Assert.True(raised); + } + + [Fact] + public void PropertyChanged_Not_Raised_When_Value_Unchanged() + { + var target = new Class1(); + var source = new Subject>(); + var raised = 0; + + target.PropertyChanged += (s, e) => ++raised; + target.Bind(Class1.FooProperty, source); + source.OnNext("newvalue"); + source.OnNext("newvalue"); + + Assert.Equal(1, raised); + } + [Fact] public void SetValue_On_Unregistered_Property_Throws_Exception() { @@ -117,6 +170,35 @@ public void SetValue_On_Unregistered_Property_Throws_Exception() Assert.Throws(() => target.SetValue(Class1.BarProperty, "value")); } + [Fact] + public void ClearValue_Restores_Default_value() + { + var target = new Class1(); + + Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Raises_PropertyChanged() + { + Class1 target = new Class1(); + var raised = 0; + + target.SetValue(Class1.FooProperty, "newvalue"); + target.PropertyChanged += (s, e) => + { + Assert.Same(target, s); + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal("newvalue", (string)e.OldValue); + Assert.Equal("unset", (string)e.NewValue); + ++raised; + }; + + target.ClearValue(Class1.FooProperty); + + Assert.Equal(1, raised); + } + [Fact] public void GetObservable_Returns_Values() { @@ -170,7 +252,7 @@ public void Bind_Binds_Property_Value_NonGeneric() } [Fact] - public void Bind_NonGeneric_Uses_UnsetValue() + public void Bind_NonGeneric_Accepts_UnsetValue() { var target = new Class1(); var source = new Subject(); @@ -194,7 +276,7 @@ public void Bind_Handles_Wrong_Type() source.OnNext(45); - Assert.Null(target.Foo); + Assert.Equal("unset", target.Foo); } [Fact] @@ -207,7 +289,7 @@ public void Bind_Handles_Wrong_Value_Type() source.OnNext("foo"); - Assert.Equal(0, target.Baz); + Assert.Equal(-1, target.Baz); } [Fact] @@ -358,31 +440,67 @@ public void Property_Notifies_Initialized() Assert.True(raised); } + [Fact] + public void Binding_Error_Reverts_To_Default_Value() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"))); + + Assert.Equal("unset", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Binding_Error_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"), "bar")); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + [Fact] public void DataValidationError_Does_Not_Cause_Target_Update() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); target.Bind(Class1.FooProperty, source); source.OnNext("initial"); - source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.DataValidationError)); + source.OnNext(BindingValue.DataValidationError(new InvalidOperationException("Foo"))); Assert.Equal("initial", target.GetValue(Class1.FooProperty)); } + [Fact] + public void DataValidationError_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.DataValidationError(new InvalidOperationException("Foo"), "bar")); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + [Fact] public void BindingError_With_FallbackValue_Causes_Target_Update() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); target.Bind(Class1.FooProperty, source); source.OnNext("initial"); - source.OnNext(new BindingNotification( - new InvalidOperationException("Foo"), - BindingErrorType.Error, - "fallback")); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"), "fallback")); Assert.Equal("fallback", target.GetValue(Class1.FooProperty)); } @@ -391,7 +509,7 @@ public void BindingError_With_FallbackValue_Causes_Target_Update() public void Binding_To_Direct_Property_Logs_BindingError() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); var called = false; LogCallback checkLogMessage = (level, area, src, mt, pv) => @@ -412,7 +530,7 @@ pv[0] is Class1 && { target.Bind(Class1.FooProperty, source); source.OnNext("baz"); - source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error)); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Binding Error Message"))); } Assert.True(called); @@ -447,7 +565,8 @@ public void AddOwner_Should_Inherit_DefaultBindingMode() "foo", o => "foo", null, - new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay)); + new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay), + false); var bar = foo.AddOwner(o => "bar"); Assert.Equal(BindingMode.TwoWay, bar.GetMetadata().DefaultBindingMode); @@ -461,7 +580,8 @@ public void AddOwner_Can_Override_DefaultBindingMode() "foo", o => "foo", null, - new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay)); + new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay), + false); var bar = foo.AddOwner(o => "bar", defaultBindingMode: BindingMode.OneWayToSource); Assert.Equal(BindingMode.TwoWay, bar.GetMetadata().DefaultBindingMode); @@ -527,10 +647,18 @@ private class Class1 : AvaloniaObject o => o.DoubleValue, (o, v) => o.DoubleValue = v); + public static readonly DirectProperty FrankProperty = + AvaloniaProperty.RegisterDirect( + nameof(Frank), + o => o.Frank, + (o, v) => o.Frank = v, + unsetValue: "Kups"); + private string _foo = "initial"; private readonly string _bar = "bar"; private int _baz = 5; private double _doubleValue; + private object _frank; public string Foo { @@ -554,6 +682,12 @@ public double DoubleValue get { return _doubleValue; } set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); } } + + public object Frank + { + get { return _frank; } + set { SetAndRaise(FrankProperty, ref _frank, value); } + } } private class Class2 : AvaloniaObject @@ -609,4 +743,4 @@ public double Value } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs index 740023fd37d..b496b30ce39 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Subjects; using Xunit; namespace Avalonia.Base.UnitTests @@ -27,11 +28,23 @@ public void GetValue_Returns_Overridden_Default_Value() [Fact] public void GetValue_Returns_Set_Value() { - Class1 target = new Class1(); + var target = new Class1(); + var property = Class1.FooProperty; + + target.SetValue(property, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(property)); + } + + [Fact] + public void GetValue_Returns_Bound_Value() + { + var target = new Class1(); + var property = Class1.FooProperty; - target.SetValue(Class1.FooProperty, "newvalue"); + target.Bind(property, new BehaviorSubject("newvalue")); - Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.Equal("newvalue", target.GetValue(property)); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index a56cd717b96..40631d04cf0 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -20,6 +20,27 @@ public void ClearValue_Clears_Value() Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void ClearValue_Raises_PropertyChanged() + { + Class1 target = new Class1(); + var raised = 0; + + target.SetValue(Class1.FooProperty, "newvalue"); + target.PropertyChanged += (s, e) => + { + Assert.Same(target, s); + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal("newvalue", (string)e.OldValue); + Assert.Equal("foodefault", (string)e.NewValue); + ++raised; + }; + + target.ClearValue(Class1.FooProperty); + + Assert.Equal(1, raised); + } + [Fact] public void SetValue_Sets_Value() { @@ -59,6 +80,25 @@ public void SetValue_Raises_PropertyChanged() Assert.True(raised); } + [Fact] + public void SetValue_Style_Priority_Raises_PropertyChanged() + { + Class1 target = new Class1(); + bool raised = false; + + target.PropertyChanged += (s, e) => + { + raised = s == target && + e.Property == Class1.FooProperty && + (string)e.OldValue == "foodefault" && + (string)e.NewValue == "newvalue"; + }; + + target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Style); + + Assert.True(raised); + } + [Fact] public void SetValue_Doesnt_Raise_PropertyChanged_If_Value_Not_Changed() { @@ -177,6 +217,28 @@ public void SetValue_Respects_Priority() Assert.Equal("three", target.GetValue(Class1.FooProperty)); } + [Fact] + public void SetValue_Style_Doesnt_Override_LocalValue() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "one", BindingPriority.LocalValue); + Assert.Equal("one", target.GetValue(Class1.FooProperty)); + target.SetValue(Class1.FooProperty, "two", BindingPriority.Style); + Assert.Equal("one", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void SetValue_LocalValue_Overrides_Style() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "one", BindingPriority.Style); + Assert.Equal("one", target.GetValue(Class1.FooProperty)); + target.SetValue(Class1.FooProperty, "two", BindingPriority.LocalValue); + Assert.Equal("two", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Setting_UnsetValue_Reverts_To_Default_Value() { @@ -188,10 +250,35 @@ public void Setting_UnsetValue_Reverts_To_Default_Value() Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void Setting_Object_Property_To_UnsetValue_Reverts_To_Default_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FrankProperty, "newvalue"); + target.SetValue(Class1.FrankProperty, AvaloniaProperty.UnsetValue); + + Assert.Equal("Kups", target.GetValue(Class1.FrankProperty)); + } + + [Fact] + public void Setting_Object_Property_To_DoNothing_Does_Nothing() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FrankProperty, "newvalue"); + target.SetValue(Class1.FrankProperty, BindingOperations.DoNothing); + + Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty)); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register("Foo", "foodefault"); + + public static readonly StyledProperty FrankProperty = + AvaloniaProperty.Register("Frank", "Kups"); } private class Class2 : Class1 diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index f0e93dbb3af..391b379c51c 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Subjects; +using Avalonia.Controls; using Xunit; namespace Avalonia.Base.UnitTests @@ -10,147 +11,87 @@ namespace Avalonia.Base.UnitTests public class AvaloniaObjectTests_Validation { [Fact] - public void SetValue_Causes_Validation() + public void Registration_Throws_If_DefaultValue_Fails_Validation() { - var target = new Class1(); - - target.SetValue(Class1.QuxProperty, 5); - Assert.Throws(() => target.SetValue(Class1.QuxProperty, 25)); - Assert.Equal(5, target.GetValue(Class1.QuxProperty)); + Assert.Throws(() => + new StyledProperty( + "BadDefault", + typeof(Class1), + new StyledPropertyMetadata(101), + validate: Class1.ValidateFoo)); } [Fact] - public void SetValue_Causes_Coercion() + public void Metadata_Override_Throws_If_DefaultValue_Fails_Validation() { - var target = new Class1(); - - target.SetValue(Class1.QuxProperty, 5); - Assert.Equal(5, target.GetValue(Class1.QuxProperty)); - target.SetValue(Class1.QuxProperty, -5); - Assert.Equal(0, target.GetValue(Class1.QuxProperty)); - target.SetValue(Class1.QuxProperty, 15); - Assert.Equal(10, target.GetValue(Class1.QuxProperty)); + Assert.Throws(() => Class1.FooProperty.OverrideDefaultValue(101)); } [Fact] - public void Revalidate_Causes_Recoercion() + public void SetValue_Throws_If_Fails_Validation() { var target = new Class1(); - target.SetValue(Class1.QuxProperty, 7); - Assert.Equal(7, target.GetValue(Class1.QuxProperty)); - target.MaxQux = 5; - target.Revalidate(Class1.QuxProperty); + Assert.Throws(() => target.SetValue(Class1.FooProperty, 101)); } [Fact] - public void Validation_Can_Be_Overridden() + public void SetValue_Throws_If_Fails_Validation_Attached() { - var target = new Class2(); - Assert.Throws(() => target.SetValue(Class1.QuxProperty, 5)); - } + var target = new Class1(); - [Fact] - public void Validation_Can_Be_Overridden_With_Null() - { - var target = new Class3(); - target.SetValue(Class1.QuxProperty, 50); - Assert.Equal(50, target.GetValue(Class1.QuxProperty)); + Assert.Throws(() => target.SetValue(Class1.AttachedProperty, 101)); } [Fact] - public void Binding_To_UnsetValue_Doesnt_Throw() + public void Reverts_To_DefaultValue_If_Binding_Fails_Validation() { var target = new Class1(); - var source = new Subject(); + var source = new Subject(); - target.Bind(Class1.QuxProperty, source); + target.Bind(Class1.FooProperty, source); + source.OnNext(150); - source.OnNext(AvaloniaProperty.UnsetValue); + Assert.Equal(11, target.GetValue(Class1.FooProperty)); } [Fact] - public void Attached_Property_Should_Be_Validated() + public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings() { - var target = new Class2(); - - target.SetValue(Class1.AttachedProperty, 15); - Assert.Equal(10, target.GetValue(Class1.AttachedProperty)); - } + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); - [Fact] - public void PropertyChanged_Event_Uses_Coerced_Value() - { - var inst = new Class1(); - inst.PropertyChanged += (sender, e) => - { - Assert.Equal(10, e.NewValue); - }; + target.Bind(Class1.FooProperty, source1); + target.Bind(Class1.FooProperty, source2); + source1.OnNext(42); + source2.OnNext(150); - inst.SetValue(Class1.QuxProperty, 15); + Assert.Equal(11, target.GetValue(Class1.FooProperty)); } private class Class1 : AvaloniaObject { - public static readonly StyledProperty QuxProperty = - AvaloniaProperty.Register("Qux", validate: Validate); + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Qux", + defaultValue: 11, + validate: ValidateFoo); public static readonly AttachedProperty AttachedProperty = - AvaloniaProperty.RegisterAttached("Attached", validate: Validate); + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + validate: ValidateFoo); - public Class1() + public static bool ValidateFoo(int value) { - MaxQux = 10; - ErrorQux = 20; - } - - public int MaxQux { get; set; } - - public int ErrorQux { get; } - - private static int Validate(Class1 instance, int value) - { - if (value > instance.ErrorQux) - { - throw new ArgumentOutOfRangeException(); - } - - return Math.Min(Math.Max(value, 0), ((Class1)instance).MaxQux); - } - - private static int Validate(Class2 instance, int value) - { - return Math.Min(value, 10); + return value < 100; } } private class Class2 : AvaloniaObject { - public static readonly StyledProperty QuxProperty = - Class1.QuxProperty.AddOwner(); - - static Class2() - { - QuxProperty.OverrideValidation(Validate); - } - - private static int Validate(Class2 instance, int value) - { - if (value < 100) - { - throw new ArgumentOutOfRangeException(); - } - - return value; - } - } - - private class Class3 : Class2 - { - static Class3() - { - QuxProperty.OverrideValidation(null); - } } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 2933893f7a9..90b8bcff63d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -140,6 +140,42 @@ public void OverrideMetadata(PropertyMetadata metadata) { OverrideMetadata(typeof(T), metadata); } + + internal override void NotifyInitialized(IAvaloniaObject o) + { + throw new NotImplementedException(); + } + + internal override IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority) + { + throw new NotImplementedException(); + } + + internal override void RouteClearValue(IAvaloniaObject o) + { + throw new NotImplementedException(); + } + + internal override object RouteGetValue(IAvaloniaObject o) + { + throw new NotImplementedException(); + } + + internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent) + { + throw new NotImplementedException(); + } + + internal override void RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority) + { + throw new NotImplementedException(); + } } private class Class1 : AvaloniaObject diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index c472fffb389..4eabc70ccaa 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -16,39 +16,39 @@ namespace Avalonia.Base.UnitTests.Data.Core { public class ExpressionObserverTests_DataValidation : IClassFixture { - [Fact] - public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() - { - var data = new ExceptionTest { MustBePositive = 5 }; - var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); - var validationMessageFound = false; - - observer.OfType() - .Where(x => x.ErrorType == BindingErrorType.DataValidationError) - .Subscribe(_ => validationMessageFound = true); - observer.SetValue(-5); - - Assert.False(validationMessageFound); - - GC.KeepAlive(data); - } - - [Fact] - public void Exception_Validation_Sends_DataValidationError() - { - var data = new ExceptionTest { MustBePositive = 5 }; - var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); - var validationMessageFound = false; - - observer.OfType() - .Where(x => x.ErrorType == BindingErrorType.DataValidationError) - .Subscribe(_ => validationMessageFound = true); - observer.SetValue(-5); - - Assert.True(validationMessageFound); - - GC.KeepAlive(data); - } + ////[Fact] + ////public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() + ////{ + //// var data = new ExceptionTest { MustBePositive = 5 }; + //// var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); + //// var validationMessageFound = false; + + //// observer.OfType() + //// .Where(x => x.ErrorType == BindingErrorType.DataValidationError) + //// .Subscribe(_ => validationMessageFound = true); + //// observer.SetValue(-5); + + //// Assert.False(validationMessageFound); + + //// GC.KeepAlive(data); + ////} + + ////[Fact] + ////public void Exception_Validation_Sends_DataValidationError() + ////{ + //// var data = new ExceptionTest { MustBePositive = 5 }; + //// var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); + //// var validationMessageFound = false; + + //// observer.OfType() + //// .Where(x => x.ErrorType == BindingErrorType.DataValidationError) + //// .Subscribe(_ => validationMessageFound = true); + //// observer.SetValue(-5); + + //// Assert.True(validationMessageFound); + + //// GC.KeepAlive(data); + ////} [Fact] public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index fe7186e4175..c2a8b03f150 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -34,8 +34,9 @@ public void IsDirect_Property_Returns_True() var target = new DirectProperty( "test", o => null, - null, - new DirectPropertyMetadata()); + null, + new DirectPropertyMetadata(), + false); Assert.True(target.IsDirect); } @@ -71,17 +72,6 @@ public void AddOwnered_Properties_Should_Share_Observables() Assert.Same(p1.Initialized, p2.Initialized); } - [Fact] - public void IsAnimating_On_DirectProperty_With_Binding_Returns_False() - { - var target = new Class1(); - var source = new BehaviorSubject("foo"); - - target.Bind(Class1.FooProperty, source, BindingPriority.Animation); - - Assert.False(target.IsAnimating(Class1.FooProperty)); - } - private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 63e1790cce2..8c76445645d 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -1,314 +1,239 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Utilities; -using Moq; -using System; +using System; using System.Linq; -using System.Reactive.Linq; -using System.Reactive.Subjects; +using System.Reactive.Disposables; +using Avalonia.Data; +using Avalonia.PropertyStore; +using Moq; using Xunit; namespace Avalonia.Base.UnitTests { public class PriorityValueTests { - private static readonly AvaloniaProperty TestProperty = - new StyledProperty( - "Test", - typeof(PriorityValueTests), - new StyledPropertyMetadata()); + private static readonly IValueSink NullSink = Mock.Of(); + private static readonly IAvaloniaObject Owner = Mock.Of(); + private static readonly StyledProperty TestProperty = new StyledProperty( + "Test", + typeof(PriorityValueTests), + new StyledPropertyMetadata()); [Fact] - public void Initial_Value_Should_Be_UnsetValue() + public void Constructor_Should_Set_Value_Based_On_Initial_Entry() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + var target = new PriorityValue( + Owner, + TestProperty, + NullSink, + new ConstantValueEntry(TestProperty, "1", BindingPriority.StyleTrigger)); - Assert.Same(AvaloniaProperty.UnsetValue, target.Value); + Assert.Equal("1", target.Value.Value); + Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority); } [Fact] - public void First_Binding_Sets_Value() + public void SetValue_LocalValue_Should_Not_Add_Entries() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + var target = new PriorityValue( + Owner, + TestProperty, + NullSink); - target.Add(Single("foo"), 0); + target.SetValue("1", BindingPriority.LocalValue); + target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("foo", target.Value); + Assert.Empty(target.Entries); } [Fact] - public void Changing_Binding_Should_Set_Value() + public void SetValue_Non_LocalValue_Should_Add_Entries() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("foo"); + var target = new PriorityValue( + Owner, + TestProperty, + NullSink); - target.Add(subject, 0); - Assert.Equal("foo", target.Value); - subject.OnNext("bar"); - Assert.Equal("bar", target.Value); - } + target.SetValue("1", BindingPriority.Style); + target.SetValue("2", BindingPriority.Animation); - [Fact] - public void Setting_Direct_Value_Should_Override_Binding() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + var result = target.Entries + .OfType>() + .Select(x => x.Value.Value) + .ToList(); - target.Add(Single("foo"), 0); - target.SetValue("bar", 0); - - Assert.Equal("bar", target.Value); + Assert.Equal(new[] { "1", "2" }, result); } [Fact] - public void Binding_Firing_Should_Override_Direct_Value() + public void Binding_With_Same_Priority_Should_Be_Appended() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var source = new BehaviorSubject("initial"); - - target.Add(source, 0); - Assert.Equal("initial", target.Value); - target.SetValue("first", 0); - Assert.Equal("first", target.Value); - source.OnNext("second"); - Assert.Equal("second", target.Value); - } + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); - [Fact] - public void Earlier_Binding_Firing_Should_Not_Override_Later() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var nonActive = new BehaviorSubject("na"); - var source = new BehaviorSubject("initial"); - - target.Add(nonActive, 1); - target.Add(source, 1); - Assert.Equal("initial", target.Value); - target.SetValue("first", 1); - Assert.Equal("first", target.Value); - nonActive.OnNext("second"); - Assert.Equal("first", target.Value); - } + target.AddBinding(source1, BindingPriority.LocalValue); + target.AddBinding(source2, BindingPriority.LocalValue); - [Fact] - public void Binding_Completing_Should_Revert_To_Direct_Value() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var source = new BehaviorSubject("initial"); - - target.Add(source, 0); - Assert.Equal("initial", target.Value); - target.SetValue("first", 0); - Assert.Equal("first", target.Value); - source.OnNext("second"); - Assert.Equal("second", target.Value); - source.OnCompleted(); - Assert.Equal("first", target.Value); - } - - [Fact] - public void Binding_With_Lower_Priority_Has_Precedence() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - - target.Add(Single("foo"), 1); - target.Add(Single("bar"), 0); - target.Add(Single("baz"), 1); + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - Assert.Equal("bar", target.Value); + Assert.Equal(new[] { "1", "2" }, result); } [Fact] - public void Later_Binding_With_Same_Priority_Should_Take_Precedence() + public void Binding_With_Higher_Priority_Should_Be_Appended() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); - target.Add(Single("foo"), 1); - target.Add(Single("bar"), 0); - target.Add(Single("baz"), 0); - target.Add(Single("qux"), 1); + target.AddBinding(source1, BindingPriority.LocalValue); + target.AddBinding(source2, BindingPriority.Animation); - Assert.Equal("baz", target.Value); - } + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - [Fact] - public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("bar"); - - target.Add(Single("foo"), 0); - target.Add(subject, 1); - Assert.Equal("foo", target.Value); - subject.OnNext("baz"); - Assert.Equal("foo", target.Value); + Assert.Equal(new[] { "1", "2" }, result); } [Fact] - public void UnsetValue_Should_Fall_Back_To_Next_Binding() + public void Binding_With_Lower_Priority_Should_Be_Prepended() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("bar"); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); - target.Add(subject, 0); - target.Add(Single("foo"), 1); + target.AddBinding(source1, BindingPriority.LocalValue); + target.AddBinding(source2, BindingPriority.Style); - Assert.Equal("bar", target.Value); + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - subject.OnNext(AvaloniaProperty.UnsetValue); - - Assert.Equal("foo", target.Value); + Assert.Equal(new[] { "2", "1" }, result); } [Fact] - public void Adding_Value_Should_Call_OnNext() + public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() { - var owner = GetMockOwner(); - var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); - - target.Add(Single("foo"), 0); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); + var source3 = new Source("3"); - owner.Verify(x => x.Changed(target.Property, target.ValuePriority, AvaloniaProperty.UnsetValue, "foo")); - } - - [Fact] - public void Changing_Value_Should_Call_OnNext() - { - var owner = GetMockOwner(); - var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("foo"); + target.AddBinding(source1, BindingPriority.LocalValue); + target.AddBinding(source2, BindingPriority.Style); + target.AddBinding(source3, BindingPriority.Style); - target.Add(subject, 0); - subject.OnNext("bar"); + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - owner.Verify(x => x.Changed(target.Property, target.ValuePriority, "foo", "bar")); + Assert.Equal(new[] { "2", "3", "1" }, result); } [Fact] - public void Disposing_A_Binding_Should_Revert_To_Next_Value() + public void Competed_Binding_Should_Be_Removed() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - - target.Add(Single("foo"), 0); - var disposable = target.Add(Single("bar"), 0); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); + var source3 = new Source("3"); - Assert.Equal("bar", target.Value); - disposable.Dispose(); - Assert.Equal("foo", target.Value); - } - - [Fact] - public void Disposing_A_Binding_Should_Remove_BindingEntry() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + target.AddBinding(source1, BindingPriority.LocalValue).Start(); + target.AddBinding(source2, BindingPriority.Style).Start(); + target.AddBinding(source3, BindingPriority.Style).Start(); + source3.OnCompleted(); - target.Add(Single("foo"), 0); - var disposable = target.Add(Single("bar"), 0); + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - Assert.Equal(2, target.GetBindings().Count()); - disposable.Dispose(); - Assert.Single(target.GetBindings()); + Assert.Equal(new[] { "2", "1" }, result); } [Fact] - public void Completing_A_Binding_Should_Revert_To_Previous_Binding() + public void Value_Should_Come_From_Last_Entry() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var source = new BehaviorSubject("bar"); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); + var source3 = new Source("3"); - target.Add(Single("foo"), 0); - target.Add(source, 0); + target.AddBinding(source1, BindingPriority.LocalValue).Start(); + target.AddBinding(source2, BindingPriority.Style).Start(); + target.AddBinding(source3, BindingPriority.Style).Start(); - Assert.Equal("bar", target.Value); - source.OnCompleted(); - Assert.Equal("foo", target.Value); + Assert.Equal("1", target.Value.Value); } [Fact] - public void Completing_A_Binding_Should_Revert_To_Lower_Priority() + public void LocalValue_Should_Override_LocalValue_Binding() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var source = new BehaviorSubject("bar"); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); - target.Add(Single("foo"), 1); - target.Add(source, 0); + target.AddBinding(source1, BindingPriority.LocalValue).Start(); + target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("bar", target.Value); - source.OnCompleted(); - Assert.Equal("foo", target.Value); + Assert.Equal("2", target.Value.Value); } [Fact] - public void Completing_A_Binding_Should_Remove_BindingEntry() + public void LocalValue_Should_Override_Style_Binding() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("bar"); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); - target.Add(Single("foo"), 0); - target.Add(subject, 0); + target.AddBinding(source1, BindingPriority.Style).Start(); + target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal(2, target.GetBindings().Count()); - subject.OnCompleted(); - Assert.Single(target.GetBindings()); + Assert.Equal("2", target.Value.Value); } [Fact] - public void Direct_Value_Should_Be_Coerced() + public void LocalValue_Should_Not_Override_Animation_Binding() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10)); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); - target.SetValue(5, 0); - Assert.Equal(5, target.Value); - target.SetValue(15, 0); - Assert.Equal(10, target.Value); - } + target.AddBinding(source1, BindingPriority.Animation).Start(); + target.SetValue("2", BindingPriority.LocalValue); - [Fact] - public void Bound_Value_Should_Be_Coerced() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10)); - var source = new Subject(); - - target.Add(source, 0); - source.OnNext(5); - Assert.Equal(5, target.Value); - source.OnNext(15); - Assert.Equal(10, target.Value); + Assert.Equal("1", target.Value.Value); } - [Fact] - public void Revalidate_Should_ReCoerce_Value() + private class Source : IObservable> { - var max = 10; - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, max)); - var source = new Subject(); - - target.Add(source, 0); - source.OnNext(5); - Assert.Equal(5, target.Value); - source.OnNext(15); - Assert.Equal(10, target.Value); - max = 12; - target.Revalidate(); - Assert.Equal(12, target.Value); - } + private IObserver> _observer; - /// - /// Returns an observable that returns a single value but does not complete. - /// - /// The type of the observable. - /// The value. - /// The observable. - private IObservable Single(T value) - { - return Observable.Never().StartWith(value); - } + public Source(string id) => Id = id; + public string Id { get; } - private static Mock GetMockOwner() - { - var owner = new Mock(); - owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny())).Returns(new DeferredSetter()); - return owner; + public IDisposable Subscribe(IObserver> observer) + { + _observer = observer; + observer.OnNext(Id); + return Disposable.Empty; + } + + public void OnCompleted() => _observer.OnCompleted(); } } } diff --git a/tests/Avalonia.Benchmarks/Base/Properties.cs b/tests/Avalonia.Benchmarks/Base/Properties.cs index 45fc68ac966..e3650d9f4bc 100644 --- a/tests/Avalonia.Benchmarks/Base/Properties.cs +++ b/tests/Avalonia.Benchmarks/Base/Properties.cs @@ -35,7 +35,7 @@ public void BindIntProperty() class Class1 : AvaloniaObject { - public static readonly AvaloniaProperty IntProperty = + public static readonly StyledProperty IntProperty = AvaloniaProperty.Register("Int"); } } diff --git a/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs new file mode 100644 index 00000000000..b70ae19275e --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Subjects; +using System.Text; +using Avalonia.Data; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class StyledPropertyBenchmarks + { + [Benchmark] + public void Set_Int_Property_LocalValue() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValue += 1; + } + } + + [Benchmark] + public void Set_Int_Property_Multiple_Priorities() + { + var obj = new StyledClass(); + var value = 0; + + for (var i = 0; i < 100; ++i) + { + for (var p = BindingPriority.Animation; p <= BindingPriority.Style; ++p) + { + obj.SetValue(StyledClass.IntValueProperty, value++, p); + } + } + } + + [Benchmark] + public void Set_Int_Property_TemplatedParent() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.SetValue(StyledClass.IntValueProperty, obj.IntValue + 1, BindingPriority.TemplatedParent); + } + } + + [Benchmark] + public void Bind_Int_Property_LocalValue() + { + var obj = new StyledClass(); + var source = new Subject>(); + + obj.Bind(StyledClass.IntValueProperty, source); + + for (var i = 0; i < 100; ++i) + { + source.OnNext(i); + } + } + + [Benchmark] + public void Bind_Int_Property_Multiple_Priorities() + { + var obj = new StyledClass(); + var sources = new List>>(); + var value = 0; + + for (var p = BindingPriority.Animation; p <= BindingPriority.Style; ++p) + { + var source = new Subject>(); + sources.Add(source); + obj.Bind(StyledClass.IntValueProperty, source, p); + } + + for (var i = 0; i < 100; ++i) + { + foreach (var source in sources) + { + source.OnNext(value++); + } + } + } + + [Benchmark] + public void Set_Validated_Int_Property_LocalValue() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.ValidatedIntValue += 1; + } + } + + [Benchmark] + public void Set_Coerced_Int_Property_LocalValue() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.CoercedIntValue += 1; + } + } + + class StyledClass : AvaloniaObject + { + public static readonly StyledProperty IntValueProperty = + AvaloniaProperty.Register(nameof(IntValue)); + public static readonly StyledProperty ValidatedIntValueProperty = + AvaloniaProperty.Register(nameof(ValidatedIntValue), validate: ValidateIntValue); + public static readonly StyledProperty CoercedIntValueProperty = + AvaloniaProperty.Register(nameof(CoercedIntValue), coerce: CoerceIntValue); + + public int IntValue + { + get => GetValue(IntValueProperty); + set => SetValue(IntValueProperty, value); + } + + public int ValidatedIntValue + { + get => GetValue(ValidatedIntValueProperty); + set => SetValue(ValidatedIntValueProperty, value); + } + + public int CoercedIntValue + { + get => GetValue(CoercedIntValueProperty); + set => SetValue(CoercedIntValueProperty, value); + } + + private static bool ValidateIntValue(int arg) + { + return arg < 1000; + } + + private static int CoerceIntValue(IAvaloniaObject arg1, int arg2) + { + return Math.Min(1000, arg2); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index f75f6fcf910..7db96a8db77 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -194,7 +194,7 @@ private PopupRoot CreateTarget(TopLevel popupParent) private class TemplatedControlWithPopup : TemplatedControl { - public static readonly AvaloniaProperty PopupContentProperty = + public static readonly StyledProperty PopupContentProperty = AvaloniaProperty.Register(nameof(PopupContent)); public TemplatedControlWithPopup() diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index cd33fae6f33..d85f6ceac84 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -798,7 +798,7 @@ private class OldDataContextTest : Control public OldDataContextTest() { - Bind(BarProperty, this.GetObservable(FooProperty)); + this.Bind(BarProperty, this.GetObservable(FooProperty)); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs index e9c3da51600..b8885369bb6 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs @@ -35,8 +35,7 @@ public void OneWay_Binding_Should_Be_Set_Up() target.Verify(x => x.Bind( TextBox.TextProperty, - It.IsAny>(), - BindingPriority.TemplatedParent)); + It.IsAny>>())); } [Fact] @@ -55,8 +54,7 @@ public void TwoWay_Binding_Should_Be_Set_Up() target.Verify(x => x.Bind( TextBox.TextProperty, - It.IsAny>(), - BindingPriority.TemplatedParent)); + It.IsAny>>())); } private Mock CreateTarget( @@ -66,9 +64,9 @@ private Mock CreateTarget( var result = new Mock(); result.Setup(x => x.GetValue(Control.TemplatedParentProperty)).Returns(templatedParent); - result.Setup(x => x.GetValue((AvaloniaProperty)Control.TemplatedParentProperty)).Returns(templatedParent); - result.Setup(x => x.GetValue((AvaloniaProperty)TextBox.TextProperty)).Returns(text); - result.Setup(x => x.Bind(It.IsAny(), It.IsAny>(), It.IsAny())) + result.Setup(x => x.GetValue(Control.TemplatedParentProperty)).Returns(templatedParent); + result.Setup(x => x.GetValue(TextBox.TextProperty)).Returns(text); + result.Setup(x => x.Bind(It.IsAny>(), It.IsAny>>())) .Returns(Disposable.Empty); return result; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs index 104f46cbac3..a5f1eaf96dd 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls; +using Avalonia.Data; using Avalonia.LogicalTree; using System.Collections.Generic; using System.ComponentModel; @@ -20,10 +21,10 @@ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e base.OnAttachedToLogicalTree(e); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { - Order.Add($"Property {e.Property.Name} Changed"); - base.OnPropertyChanged(e); + Order.Add($"Property {property.Name} Changed"); + base.OnPropertyChanged(property, oldValue, newValue, priority); } void ISupportInitialize.BeginInit() diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index 320dbfeec39..56735cc90bb 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -88,6 +88,76 @@ public ILogical LogicalParent get => Parent; set => ((ISetLogicalParent)this).SetParent(value); } + + public void ClearValue(AvaloniaProperty property) + { + throw new NotImplementedException(); + } + + public void ClearValue(AvaloniaProperty property) + { + throw new NotImplementedException(); + } + + public void AddInheritanceChild(IAvaloniaObject child) + { + throw new NotImplementedException(); + } + + public void RemoveInheritanceChild(IAvaloniaObject child) + { + throw new NotImplementedException(); + } + + public void InheritanceParentChanged(StyledPropertyBase property, IAvaloniaObject oldParent, IAvaloniaObject newParent) + { + throw new NotImplementedException(); + } + + public void InheritedPropertyChanged(AvaloniaProperty property, Optional oldValue, Optional newValue) + { + throw new NotImplementedException(); + } + + public void ClearValue(StyledPropertyBase property) + { + throw new NotImplementedException(); + } + + public void ClearValue(DirectPropertyBase property) + { + throw new NotImplementedException(); + } + + public T GetValue(StyledPropertyBase property) + { + throw new NotImplementedException(); + } + + public T GetValue(DirectPropertyBase property) + { + throw new NotImplementedException(); + } + + public void SetValue(StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + public void SetValue(DirectPropertyBase property, T value) + { + throw new NotImplementedException(); + } + + public IDisposable Bind(StyledPropertyBase property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + public IDisposable Bind(DirectPropertyBase property, IObservable> source) + { + throw new NotImplementedException(); + } } public class TestLogical1 : TestLogical diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index 940b2b18ef5..099562b1cf3 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -118,6 +118,76 @@ public ILogical LogicalParent get => Parent; set => ((ISetLogicalParent)this).SetParent(value); } + + public void ClearValue(AvaloniaProperty property) + { + throw new NotImplementedException(); + } + + public void ClearValue(AvaloniaProperty property) + { + throw new NotImplementedException(); + } + + public void AddInheritanceChild(IAvaloniaObject child) + { + throw new NotImplementedException(); + } + + public void RemoveInheritanceChild(IAvaloniaObject child) + { + throw new NotImplementedException(); + } + + public void InheritanceParentChanged(StyledPropertyBase property, IAvaloniaObject oldParent, IAvaloniaObject newParent) + { + throw new NotImplementedException(); + } + + public void InheritedPropertyChanged(AvaloniaProperty property, Optional oldValue, Optional newValue) + { + throw new NotImplementedException(); + } + + public void ClearValue(StyledPropertyBase property) + { + throw new NotImplementedException(); + } + + public void ClearValue(DirectPropertyBase property) + { + throw new NotImplementedException(); + } + + public T GetValue(StyledPropertyBase property) + { + throw new NotImplementedException(); + } + + public T GetValue(DirectPropertyBase property) + { + throw new NotImplementedException(); + } + + public void SetValue(StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + public void SetValue(DirectPropertyBase property, T value) + { + throw new NotImplementedException(); + } + + public IDisposable Bind(StyledPropertyBase property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + public IDisposable Bind(DirectPropertyBase property, IObservable> source) + { + throw new NotImplementedException(); + } } public class TestLogical1 : TestLogical diff --git a/tests/Avalonia.Styling.UnitTests/SetterTests.cs b/tests/Avalonia.Styling.UnitTests/SetterTests.cs index 52f75627f9a..7f63f2b05ae 100644 --- a/tests/Avalonia.Styling.UnitTests/SetterTests.cs +++ b/tests/Avalonia.Styling.UnitTests/SetterTests.cs @@ -83,8 +83,7 @@ public void Setter_Should_Apply_Value_Without_Activator_With_Style_Priority() control.Verify(x => x.Bind( TextBlock.TextProperty, - It.IsAny>(), - BindingPriority.Style)); + It.IsAny>>())); } [Fact] @@ -99,8 +98,7 @@ public void Setter_Should_Apply_Value_With_Activator_With_StyleTrigger_Priority( control.Verify(x => x.Bind( TextBlock.TextProperty, - It.IsAny>(), - BindingPriority.StyleTrigger)); + It.IsAny>>())); } [Fact] @@ -114,8 +112,7 @@ public void Setter_Should_Apply_Binding_Without_Activator_With_Style_Priority() control.Verify(x => x.Bind( TextBlock.TextProperty, - It.IsAny>(), - BindingPriority.Style)); + It.IsAny>>())); } [Fact] @@ -130,8 +127,7 @@ public void Setter_Should_Apply_Binding_With_Activator_With_StyleTrigger_Priorit control.Verify(x => x.Bind( TextBlock.TextProperty, - It.IsAny>(), - BindingPriority.StyleTrigger)); + It.IsAny>>())); } private IBinding CreateMockBinding(AvaloniaProperty property) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs index b046910f343..7b1ed7f9773 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs @@ -84,7 +84,7 @@ public void Transform_Produces_Transformed_PlatformImpl() private class TestGeometry : Geometry { - public static readonly AvaloniaProperty FooProperty = + public static readonly StyledProperty FooProperty = AvaloniaProperty.Register(nameof(Foo)); static TestGeometry()