From fc2439e0cffe599a44abade756023a2d84603120 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Nov 2019 15:15:26 +0100 Subject: [PATCH 01/50] Added some benchmarks for styled properties. --- tests/Avalonia.Benchmarks/Base/Properties.cs | 2 +- .../Base/StyledPropertyBenchmark.cs | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs 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..4b57776759d --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs @@ -0,0 +1,101 @@ +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++); + } + } + } + + class StyledClass : AvaloniaObject + { + private int _intValue; + + public static readonly StyledProperty IntValueProperty = + AvaloniaProperty.Register(nameof(IntValue)); + + public int IntValue + { + get => GetValue(IntValueProperty); + set => SetValue(IntValueProperty, value); + } + } + } +} From 6be3acb46c8c70379c6099c3a0262b9bac15b49e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Nov 2019 15:16:37 +0100 Subject: [PATCH 02/50] Make ValueStore typed. Major refactor of the Avalonia core to make the styled property store typed. --- src/Avalonia.Animation/Animatable.cs | 26 +- src/Avalonia.Base/AvaloniaObject.cs | 729 +++++++++--------- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 146 ++++ src/Avalonia.Base/AvaloniaProperty.cs | 89 ++- .../AvaloniaPropertyChangedEventArgs.cs | 40 +- .../AvaloniaPropertyChangedEventArgs`1.cs | 67 ++ src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 115 ++- src/Avalonia.Base/AvaloniaProperty`1.cs | 33 +- src/Avalonia.Base/Data/BindingNotification.cs | 20 + src/Avalonia.Base/Data/BindingOperations.cs | 14 +- src/Avalonia.Base/Data/BindingValue.cs | 406 ++++++++++ src/Avalonia.Base/Data/Optional.cs | 129 ++++ .../Diagnostics/AvaloniaObjectExtensions.cs | 58 +- src/Avalonia.Base/DirectProperty.cs | 98 ++- src/Avalonia.Base/DirectPropertyBase.cs | 159 ++++ src/Avalonia.Base/IAvaloniaObject.cs | 54 +- src/Avalonia.Base/IPriorityValueOwner.cs | 51 -- src/Avalonia.Base/IStyledPropertyAccessor.cs | 9 - src/Avalonia.Base/IStyledPropertyMetadata.cs | 7 +- src/Avalonia.Base/PriorityBindingEntry.cs | 160 ---- src/Avalonia.Base/PriorityLevel.cs | 227 ------ src/Avalonia.Base/PriorityValue.cs | 315 -------- .../PropertyStore/BindingEntry.cs | 95 +++ .../PropertyStore/ConstantValueEntry.cs | 28 + .../PropertyStore/IPriorityValueEntry.cs | 18 + src/Avalonia.Base/PropertyStore/IValue.cs | 17 + src/Avalonia.Base/PropertyStore/IValueSink.cs | 18 + .../PropertyStore/PriorityValue.cs | 143 ++++ .../AvaloniaPropertyBindingObservable.cs | 55 ++ .../Reactive/BindingValueAdapter.cs | 61 ++ .../Reactive/BindingValueExtensions.cs | 35 + .../Reactive/TypedBindingAdapter.cs | 63 ++ .../Reactive/UntypedBindingAdapter.cs | 57 ++ src/Avalonia.Base/StyledPropertyBase.cs | 84 +- src/Avalonia.Base/StyledPropertyMetadata`1.cs | 15 - .../Utilities/AvaloniaPropertyValueStore.cs | 21 + src/Avalonia.Base/Utilities/TypeUtilities.cs | 11 + src/Avalonia.Base/ValueStore.cs | 282 ++++--- src/Avalonia.Controls.DataGrid/DataGrid.cs | 24 +- .../DataGridRowGroupHeader.cs | 4 +- src/Avalonia.Controls/AutoCompleteBox.cs | 16 +- src/Avalonia.Controls/Button.cs | 18 +- src/Avalonia.Controls/Calendar/Calendar.cs | 4 +- src/Avalonia.Controls/Calendar/DatePicker.cs | 18 +- src/Avalonia.Controls/MenuItem.cs | 10 +- .../NumericUpDown/NumericUpDown.cs | 6 +- src/Avalonia.Controls/Primitives/ScrollBar.cs | 2 +- .../Repeater/ItemsRepeater.cs | 27 +- src/Avalonia.Controls/ScrollViewer.cs | 2 - src/Avalonia.Controls/TextBox.cs | 8 +- src/Avalonia.Layout/StackLayout.cs | 7 +- src/Avalonia.Layout/UniformGridLayout.cs | 33 +- src/Avalonia.Styling/StyledElement.cs | 6 +- src/Avalonia.Visuals/Visual.cs | 6 +- .../AvaloniaObjectTests_AddOwner.cs | 22 +- .../AvaloniaObjectTests_Attached.cs | 8 - .../AvaloniaObjectTests_Binding.cs | 255 +++++- .../AvaloniaObjectTests_DataValidation.cs | 101 +-- .../AvaloniaObjectTests_Direct.cs | 172 ++++- .../AvaloniaObjectTests_GetValue.cs | 19 +- .../AvaloniaObjectTests_SetValue.cs | 87 +++ .../AvaloniaObjectTests_Validation.cs | 156 ---- .../AvaloniaPropertyTests.cs | 31 + .../ExpressionObserverTests_DataValidation.cs | 66 +- .../DirectPropertyTests.cs | 16 +- .../PriorityValueTests.cs | 371 ++++----- .../Data/BindingTests.cs | 2 +- .../Data/BindingTests_TemplatedParent.cs | 6 +- .../Xaml/InitializationOrderTracker.cs | 7 +- .../SelectorTests_Child.cs | 34 +- .../SelectorTests_Descendent.cs | 34 +- .../Avalonia.Styling.UnitTests/SetterTests.cs | 8 +- .../TestControlBase.cs | 34 +- .../TestTemplatedControl.cs | 34 +- 74 files changed, 3506 insertions(+), 2103 deletions(-) create mode 100644 src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs create mode 100644 src/Avalonia.Base/Data/BindingValue.cs create mode 100644 src/Avalonia.Base/Data/Optional.cs create mode 100644 src/Avalonia.Base/DirectPropertyBase.cs delete mode 100644 src/Avalonia.Base/IPriorityValueOwner.cs delete mode 100644 src/Avalonia.Base/PriorityBindingEntry.cs delete mode 100644 src/Avalonia.Base/PriorityLevel.cs delete mode 100644 src/Avalonia.Base/PriorityValue.cs create mode 100644 src/Avalonia.Base/PropertyStore/BindingEntry.cs create mode 100644 src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs create mode 100644 src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs create mode 100644 src/Avalonia.Base/PropertyStore/IValue.cs create mode 100644 src/Avalonia.Base/PropertyStore/IValueSink.cs create mode 100644 src/Avalonia.Base/PropertyStore/PriorityValue.cs create mode 100644 src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs create mode 100644 src/Avalonia.Base/Reactive/BindingValueAdapter.cs create mode 100644 src/Avalonia.Base/Reactive/BindingValueExtensions.cs create mode 100644 src/Avalonia.Base/Reactive/TypedBindingAdapter.cs create mode 100644 src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs delete mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index ca45fb8c4d4..ac2fd5b9845 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.ValueOrDefault(), + newValue.ValueOrDefault()); - _previousTransitions[e.Property] = instance; + _previousTransitions[property] = instance; return; } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 0499907ab82..0ce468354cc 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -4,11 +4,10 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; +using Avalonia.PropertyStore; using Avalonia.Threading; using Avalonia.Utilities; @@ -20,13 +19,13 @@ namespace Avalonia /// /// 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 +56,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 +73,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); } } } @@ -167,9 +137,31 @@ public IBinding this[IndexerDescriptor binding] public void ClearValue(AvaloniaProperty property) { Contract.Requires(property != null); + property.RouteClearValue(this); + } + + /// + /// Clears a 's local value. + /// + /// The property. + public void ClearValue(AvaloniaProperty property) + { VerifyAccess(); - SetValue(property, AvaloniaProperty.UnsetValue); + switch (property) + { + case StyledPropertyBase styled: + _values.ClearLocalValue(styled); + break; + case DirectPropertyBase direct: + var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, direct); + p.InvokeSetter(this, p.GetUnsetValue(GetType())); + break; + case null: + throw new ArgumentNullException(nameof(property)); + default: + throw new NotSupportedException("Unsupported AvaloniaProperty type."); + } } /// @@ -210,21 +202,38 @@ public void ClearValue(AvaloniaProperty property) /// The value. public object GetValue(AvaloniaProperty property) { - if (property is null) + return property.RouteGetValue(this); + } + + /// + /// Gets a value. + /// + /// The type of the property. + /// The property. + /// The value. + public T GetValue(AvaloniaProperty property) + { + return property switch { - throw new ArgumentNullException(nameof(property)); - } + StyledPropertyBase styled => GetValue(styled), + DirectPropertyBase direct => GetValue(direct), + null => throw new ArgumentNullException(nameof(property)), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; + } + /// + /// 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 +242,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 +292,33 @@ public void SetValue( object value, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - VerifyAccess(); + property.RouteSetValue(this, value, priority); + } - if (property.IsDirect) - { - SetDirectValue(property, value); - } - else + /// + /// Sets a value. + /// + /// 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) + { + switch (property) { - SetStyledValue(property, value, priority); + case StyledPropertyBase styled: + SetValue(styled, value, priority); + break; + case DirectPropertyBase direct: + SetValue(direct, value); + break; + case null: + throw new ArgumentNullException(nameof(property)); + default: + throw new NotSupportedException("Unsupported AvaloniaProperty type."); } } @@ -305,13 +330,46 @@ public void SetValue( /// The value. /// The priority of the value. public void SetValue( - AvaloniaProperty property, + StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); - SetValue((AvaloniaProperty)property, value, priority); + LogPropertySet(property, value, priority); + + if (value is UnsetValueType) + { + if (priority == BindingPriority.LocalValue) + { + Values.ClearLocalValue(property); + } + else + { + throw new NotSupportedException( + "Canot set property to Unset at non-local value priority."); + } + } + else if (!(value is DoNothingType)) + { + Values.SetValue(property, value, priority); + } + } + + /// + /// Sets a value. + /// + /// The type of the property. + /// The property. + /// The value. + public void SetValue(DirectPropertyBase property, T value) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + LogPropertySet(property, value, BindingPriority.LocalValue); + SetDirectValueUnchecked(property, value); } /// @@ -325,47 +383,34 @@ public void SetValue( /// public IDisposable Bind( AvaloniaProperty property, - IObservable source, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - Contract.Requires(source != null); - - VerifyAccess(); - - if (property.IsDirect) - { - if (property.IsReadOnly) - { - throw new ArgumentException($"The property {property.Name} is readonly."); - } - - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority LocalValue", - property, - GetDescription(source)); - - if (_directBindings == null) - { - _directBindings = new List(); - } + return property.RouteBind(this, source, priority); + } - return new DirectBindingSubscription(this, property, source); - } - else + /// + /// 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, + BindingPriority priority = BindingPriority.LocalValue) + { + return property switch { - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority {Priority}", - property, - GetDescription(source), - priority); - - return Values.AddBinding(property, source, priority); - } + StyledPropertyBase styled => Bind(styled, source, priority), + DirectPropertyBase direct => Bind(direct, source), + null => throw new ArgumentNullException(nameof(property)), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), + }; } /// @@ -379,37 +424,96 @@ public IDisposable Bind( /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - AvaloniaProperty property, - IObservable source, + StyledPropertyBase property, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); - return Bind(property, source.Select(x => (object)x), priority); + return Values.AddBinding(property, source, priority); } /// - /// Forces the specified property to be revalidated. + /// Binds a to an observable. /// + /// The type of the property. /// The property. - public void Revalidate(AvaloniaProperty property) + /// The observable. + /// + /// A disposable which can be used to terminate the binding. + /// + public IDisposable Bind( + DirectPropertyBase property, + IObservable> source) { + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); - _values?.Revalidate(property); + + property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + + if (property.IsReadOnly) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } + + Logger.TryGet(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "Bound {Property} to {Binding} with priority LocalValue", + property, + GetDescription(source)); + + _directBindings ??= new List(); + + return new DirectBindingSubscription(this, property, source); + } + + /// + 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) { - oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? - GetDefaultValue(property) : - oldValue; - newValue = (newValue == AvaloniaProperty.UnsetValue) ? - GetDefaultValue(property) : - newValue; + _inheritanceChildren?.Remove(child); + } - if (!Equals(oldValue, newValue)) + void IAvaloniaObject.InheritedPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + Optional newValue) + { + if (property.Inherits && !IsSet(property)) { - RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); + } + } + + /// + Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() + { + return _propertyChanged?.GetInvocationList(); + } + + 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, priority); Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, @@ -421,39 +525,32 @@ 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(); - - /// - /// 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) + /// The type of the property value. + /// The property. + /// The old inheritance parent. + internal void InheritanceParentChanged( + StyledPropertyBase property, + IAvaloniaObject oldParent) { - property.Changed.Subscribe(e => + var oldValue = oldParent switch { - foreach (var p in affected) - { - e.Sender.Revalidate(p); - } - }); + AvaloniaObject o => o.GetValueOrInheritedOrDefault(property), + null => property.GetDefaultValue(GetType()), + _ => oldParent.GetValue(property) + }; + + var newValue = GetInheritedOrDefault(property); + + if (!EqualityComparer.Default.Equals(oldValue, newValue)) + { + RaisePropertyChanged(property, oldValue, newValue); + } } /// @@ -477,18 +574,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 +603,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 +682,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) + private T GetValueOrInheritedOrDefault(StyledPropertyBase property) { - if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValueOrDefaultUnchecked(property); - return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); - } + var o = this; + var inherits = property.Inherits; + var value = default(T); - /// - /// Gets the value or default value for a property. - /// - /// The property. - /// The default value. - private object GetValueOrDefaultUnchecked(AvaloniaProperty property) - { - var aobj = this; - var valuestore = aobj._values; - if (valuestore != null) + while (o != null) { - var result = valuestore.GetValue(property); - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - } - if (property.Inherits) - { - while (aobj.InheritanceParent is AvaloniaObject parent) - { - aobj = parent; - valuestore = aobj._values; - if (valuestore != null) - { - var result = valuestore.GetValue(property); - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - } - } - } - return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - } - - /// - /// Sets the value of a direct property. - /// - /// The property. - /// The value. - private void SetDirectValue(AvaloniaProperty property, object value) - { - void Set() - { - var notification = value as BindingNotification; + var values = o._values; - if (notification != null) + if (values?.TryGetValue(property, out value) == true) { - LogIfError(property, notification); - value = notification.Value; + return value; } - if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue) + if (!inherits) { - var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType()); - var accessor = (IDirectPropertyAccessor)GetRegistered(property); - var finalValue = value == AvaloniaProperty.UnsetValue ? - metadata.UnsetValue : value; - - LogPropertySet(property, value, BindingPriority.LocalValue); - - accessor.SetValue(this, finalValue); + break; } - if (notification != null) - { - UpdateDataValidation(property, notification); - } + o = o.InheritanceParent as AvaloniaObject; } - if (Dispatcher.UIThread.CheckAccess()) - { - Set(); - } - else - { - Dispatcher.UIThread.Post(Set); - } + return property.GetDefaultValue(GetType()); } /// - /// 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 +787,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 +797,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 +811,7 @@ private void LogIfError(AvaloniaProperty property, BindingNotification notificat } else { - LogBindingError(property, notification.Error); + LogBindingError(property, value.Error); } } } @@ -814,7 +822,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 +833,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 +858,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..6a513231d5d 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 /// . @@ -134,6 +179,107 @@ 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) + { + 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) + { + return target.Bind( + property, + source.ToBindingValue(), + priority); + } + /// /// Binds a property on an to an . /// diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index ac7d2c60afa..1bda55285e1 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,6 @@ 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 method that gets called before and after the property starts being notified on an /// object; the bool argument will be true before and false afterwards. This callback is @@ -267,7 +268,6 @@ public static StyledProperty Register( TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null, Action notifying = null) where TOwner : IAvaloniaObject { @@ -275,7 +275,6 @@ public static StyledProperty Register( var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), defaultBindingMode: defaultBindingMode); var result = new StyledProperty( @@ -298,7 +297,6 @@ 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 public static AttachedProperty RegisterAttached( string name, @@ -312,7 +310,6 @@ public static AttachedProperty RegisterAttached( var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), defaultBindingMode: defaultBindingMode); var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits); @@ -332,7 +329,6 @@ 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 public static AttachedProperty RegisterAttached( string name, @@ -347,7 +343,6 @@ public static AttachedProperty RegisterAttached( var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), defaultBindingMode: defaultBindingMode); var result = new AttachedProperty(name, ownerType, metadata, inherits); @@ -365,9 +360,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 +376,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 +481,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 +505,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 +595,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..0c7cd87897e --- /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.ValueOrDefault(AvaloniaProperty.UnsetValue); + + protected override object? GetNewValue() => NewValue.ValueOrDefault(AvaloniaProperty.UnsetValue); + } +} diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index d718f5917c9..4806abac4d7 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. /// @@ -192,7 +243,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); @@ -200,6 +251,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. /// @@ -265,6 +344,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); @@ -318,18 +413,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(); @@ -370,9 +453,7 @@ void Notify(AvaloniaProperty property, object value) 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..566a1531359 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,34 @@ protected AvaloniaProperty( : base(source, ownerType, metadata) { } + + internal override void RouteClearValue(IAvaloniaObject o) + { + o.ClearValue(this); + } + + 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/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index 7c55321a805..a568c062d09 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -236,6 +236,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 44b47329ac1..653388958af 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -5,12 +5,13 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Data { public static class BindingOperations { - public static readonly object DoNothing = new object(); + public static readonly object DoNothing = new DoNothingType(); /// /// Applies an a property on an . @@ -77,4 +78,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..8265a6cd539 --- /dev/null +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -0,0 +1,406 @@ +using System; +using Avalonia.Utilities; + +#nullable enable + +namespace Avalonia.Data +{ + /// + /// Describes the type of a . + /// + 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("Invalida 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 a default value. + /// + /// The default value. + /// The value. + public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue; + + /// + /// 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 ValueOrDefault(TResult defaultValue = default) + { + 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<>."); + } + } + } + + 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..de7d6a307d9 --- /dev/null +++ b/src/Avalonia.Base/Data/Optional.cs @@ -0,0 +1,129 @@ +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 struct Optional + { + 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 override int GetHashCode() => HasValue ? Value!.GetHashCode() : 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 a default value. + /// + /// The default value. + /// The value. + public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue; + + /// + /// 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 ValueOrDefault(TResult defaultValue = default) + { + 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..4885f77d9c4 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,36 @@ public static class AvaloniaObjectExtensions /// public static AvaloniaPropertyValue GetDiagnostic(this AvaloniaObject o, AvaloniaProperty property) { - var set = o.GetSetValues(); + throw new NotImplementedException(); + ////var set = o.GetSetValues(); - if (set.TryGetValue(property, out var obj)) - { - if (obj is PriorityValue value) - { - return new AvaloniaPropertyValue( - property, - o.GetValue(property), - (BindingPriority)value.ValuePriority, - value.GetDiagnostic()); - } - else - { - return new AvaloniaPropertyValue( - property, - obj, - BindingPriority.LocalValue, - "Local value"); - } - } - else - { - return new AvaloniaPropertyValue( - property, - o.GetValue(property), - BindingPriority.Unset, - "Unset"); - } + ////if (set.TryGetValue(property, out var obj)) + ////{ + //// if (obj is PriorityValue value) + //// { + //// return new AvaloniaPropertyValue( + //// property, + //// o.GetValue(property), + //// (BindingPriority)value.ValuePriority, + //// value.GetDiagnostic()); + //// } + //// else + //// { + //// return new AvaloniaPropertyValue( + //// property, + //// obj, + //// BindingPriority.LocalValue, + //// "Local value"); + //// } + ////} + ////else + ////{ + //// return new AvaloniaPropertyValue( + //// property, + //// o.GetValue(property), + //// BindingPriority.Unset, + //// "Unset"); + ////} } } } diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 1ce73c20ba2..30eb9cf69c0 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) { @@ -133,5 +201,15 @@ void IDirectPropertyAccessor.SetValue(IAvaloniaObject instance, object value) Setter((TOwner)instance, (TValue)value); } + + internal void WrapSetter(TOwner instance, BindingValue value) + { + if (Setter == null) + { + throw new ArgumentException($"The property {Name} is readonly."); + } + + Setter(instance, value.Value); + } } } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs new file mode 100644 index 00000000000..0b3747a374a --- /dev/null +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -0,0 +1,159 @@ +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) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + this, + default, + InvokeGetter(o), + BindingPriority.Unset); + NotifyInitialized(e); + } + + /// + 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, priority); + } + 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, priority); + } + + 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..066f9121eb0 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -17,9 +17,16 @@ public interface IAvaloniaObject event EventHandler PropertyChanged; /// - /// Raised when an inheritable value changes on this object. + /// Clears a 's local value. /// - event EventHandler InheritablePropertyChanged; + /// The property. + public void ClearValue(AvaloniaProperty property); + + /// + /// Clears a 's local value. + /// + /// The property. + public void ClearValue(AvaloniaProperty property); /// /// Gets a value. @@ -84,7 +91,7 @@ void SetValue( /// IDisposable Bind( AvaloniaProperty property, - IObservable source, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue); /// @@ -99,7 +106,46 @@ IDisposable Bind( /// IDisposable Bind( AvaloniaProperty property, - IObservable source, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue); + + /// + /// Registers an object as an inheritance child. + /// + /// The inheritance child. + /// + /// Inheritance children will recieve 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); + + //void InheritanceParentChanged( + // StyledPropertyBase property, + // IAvaloniaObject oldParent, + // IAvaloniaObject newParent); + + /// + /// Called when an inheritable property changes on an object registered as an inheritance + /// parent. + /// + /// The type of the value. + /// The property that has changed. + /// The old property value. + /// The new property value. + void InheritedPropertyChanged( + AvaloniaProperty property, + 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..2a7cf098a56 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -0,0 +1,95 @@ +using System; +using Avalonia.Data; +using Avalonia.Threading; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + internal interface IBindingEntry : IPriorityValueEntry, IDisposable + { + } + + internal class BindingEntry : 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.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..bc75eac4ef9 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + 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; private set; } + 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..8e239e03c93 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + internal interface IPriorityValueEntry : IValue + { + BindingPriority Priority { get; } + + void Reparent(IValueSink sink); + } + + internal interface IPriorityValueEntry : IPriorityValueEntry, IValue + { + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs new file mode 100644 index 00000000000..7d1eaa337fc --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -0,0 +1,17 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + internal interface IValue + { + Optional Value { get; } + BindingPriority ValuePriority { get; } + } + + 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..faccd9e75ae --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + internal interface IValueSink + { + void ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue); + + void Completed(AvaloniaProperty property, IPriorityValueEntry entry); + } +} diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs new file mode 100644 index 00000000000..e33ab48353c --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + internal class PriorityValue : IValue, IValueSink + { + private readonly IAvaloniaObject _owner; + private readonly IValueSink _sink; + private readonly List> _entries = new List>(); + private Optional _localValue; + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink) + { + _owner = owner; + Property = property; + _sink = sink; + } + + 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 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; + } + + void IValueSink.ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue) + { + _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); + + for (var i = _entries.Count - 1; i >= 0; --i) + { + var entry = _entries[i]; + + if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) + { + reachedLocalValues = true; + + if (_localValue.HasValue) + { + value = _localValue; + ValuePriority = BindingPriority.LocalValue; + break; + } + } + + if (entry.Value.HasValue) + { + value = entry.Value; + ValuePriority = entry.Priority; + break; + } + } + + if (value != Value) + { + var old = Value; + Value = value; + _sink.ValueChanged(Property, ValuePriority, old, value); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs new file mode 100644 index 00000000000..1bb4d917d24 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + private T _value; + + public AvaloniaPropertyBindingObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(target); + _property = property; + } + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + _value = (T)target.GetValue(_property); + target.PropertyChanged += PropertyChanged; + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } + } + + protected override void Subscribed(IObserver> observer, bool first) + { + observer.OnNext(new BindingValue(_value)); + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + _value = (T)e.NewValue; + PublishNext(new BindingValue(_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..6871602ad9f --- /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/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 27a502246a6..bbb47d63ad0 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -2,14 +2,17 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia { /// /// Base class for styled properties. /// - public class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor + public abstract class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor { private bool _inherits; @@ -124,48 +127,75 @@ public void OverrideMetadata(Type type, StyledPropertyMetadata 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) + /// + object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + /// + internal override void NotifyInitialized(IAvaloniaObject o) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + this, + default, + o.GetValue(this), + BindingPriority.Unset); + NotifyInitialized(e); + } + + /// + 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) { - f = Cast(validate); + o.SetValue(this, (TValue)v.Value, priority); } - else + else if (v.Type == BindingValueType.UnsetValue) { - // Passing null to the validation function means that the property metadata merge - // will take the base validation function, so instead use an empty validation. - f = (o, v) => v; + o.ClearValue(this); + } + else if (v.HasError) + { + throw v.Error; } - - base.OverrideMetadata(typeof(THost), new StyledPropertyMetadata(validate: f)); } - /// - /// Gets the string representation of the property. - /// - /// The property's string representation. - public override string ToString() + /// + internal override IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority) { - return Name; + var adapter = TypedBindingAdapter.Create(o, this, source); + return o.Bind(this, adapter, priority); } /// - Func IStyledPropertyAccessor.GetValidationFunc(Type type) + internal override void RouteInheritanceParentChanged( + AvaloniaObject o, + IAvaloniaObject oldParent) { - Contract.Requires(type != null); - return ((IStyledPropertyMetadata)base.GetMetadata(type)).Validate; + o.InheritanceParentChanged(this, oldParent); } - /// - object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); - private object GetDefaultBoxedValue(Type type) { Contract.Requires(type != null); diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index d1a0e2dc533..d4ce137e0a4 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -16,16 +16,13 @@ public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyM /// Initializes a new instance of the class. /// /// The default value of the property. - /// A validation function. /// The default binding mode. public StyledPropertyMetadata( TValue defaultValue = default, - Func validate = null, BindingMode defaultBindingMode = BindingMode.Default) : base(defaultBindingMode) { DefaultValue = new BoxedValue(defaultValue); - Validate = validate; } /// @@ -33,15 +30,8 @@ public StyledPropertyMetadata( /// internal BoxedValue DefaultValue { get; private set; } - /// - /// Gets the validation callback. - /// - public Func Validate { get; private set; } - object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed; - Func IStyledPropertyMetadata.Validate => Cast(Validate); - /// public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property) { @@ -53,11 +43,6 @@ public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty prope { DefaultValue = src.DefaultValue; } - - if (Validate == null) - { - Validate = src.Validate; - } } } 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/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..f6f489ef2c2 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,205 +1,231 @@ using System; using System.Collections.Generic; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Utilities; +#nullable enable + namespace Avalonia { - internal class ValueStore : IPriorityValueOwner + internal class ValueStore : IValueSink { - private readonly AvaloniaPropertyValueStore _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)) + if (_values.TryGetValue(property, out var slot)) { - priorityValue = v as PriorityValue; - - if (priorityValue == null) + if (slot is IValue v) { - priorityValue = CreatePriorityValue(property); - priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValue(property, priorityValue); + return v.ValuePriority < BindingPriority.LocalValue; } } - else - { - priorityValue = CreatePriorityValue(property); - _propertyValues.AddValue(property, priorityValue); - } - return priorityValue.Add(source, (int)priority); + return false; } - public void AddValue(AvaloniaProperty property, object value, int priority) + public bool IsSet(AvaloniaProperty property) { - PriorityValue priorityValue; + return TryGetValueUntyped(property, out _); + } - if (_propertyValues.TryGetValue(property, out var v)) + public bool TryGetValue(StyledPropertyBase property, out T value) + { + if (_values.TryGetValue(property, out var slot)) { - priorityValue = v as PriorityValue; - - if (priorityValue == null) + if (slot is IValue v) { - if (priority == (int)BindingPriority.LocalValue) - { - Validate(property, ref value); - _propertyValues.SetValue(property, value); - Changed(property, priority, v, value); - return; - } - else + if (v.Value.HasValue) { - priorityValue = CreatePriorityValue(property); - priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValue(property, priorityValue); + value = v.Value.Value; + return true; } } - } - else - { - if (value == AvaloniaProperty.UnsetValue) + else { - return; + value = (T)slot; + return true; } + } + + value = default!; + return false; + } - if (priority == (int)BindingPriority.LocalValue) + public bool TryGetValueUntyped(AvaloniaProperty property, out object? value) + { + if (_values.TryGetValue(property, out var slot)) + { + if (slot is IValue v) { - Validate(property, ref value); - _propertyValues.AddValue(property, value); - Changed(property, priority, AvaloniaProperty.UnsetValue, value); - return; + if (v.Value.HasValue) + { + value = v.Value.Value; + return true; + } } else { - priorityValue = CreatePriorityValue(property); - _propertyValues.AddValue(property, priorityValue); + value = slot; + return true; } } - priorityValue.SetValue(value, priority); - } - - public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) - { - _owner.BindingNotificationReceived(property, notification); - } - - public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) - { - _owner.PriorityValueChanged(property, priority, oldValue, newValue); + value = default; + return false; } - public IDictionary GetSetValues() + public void SetValue(StyledPropertyBase property, T value, BindingPriority priority) { - return _propertyValues.ToDictionary(); + if (_values.TryGetValue(property, out var slot)) + { + SetExisting(slot, property, value, priority); + } + else if (priority == BindingPriority.LocalValue) + { + _values.AddValue(property, (object)value!); + _sink.ValueChanged(property, priority, default, value); + } + else + { + var entry = new ConstantValueEntry(property, value, priority); + _values.AddValue(property, entry); + _sink.ValueChanged(property, priority, default, value); + } } - public void LogError(AvaloniaProperty property, Exception e) + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) { - _owner.LogBindingError(property, e); + if (_values.TryGetValue(property, out var slot)) + { + return BindExisting(slot, property, source, priority); + } + else + { + var entry = new BindingEntry(_owner, property, source, priority, this); + _values.AddValue(property, entry); + entry.Start(); + return entry; + } } - public object GetValue(AvaloniaProperty property) + public void ClearLocalValue(StyledPropertyBase property) { - var result = AvaloniaProperty.UnsetValue; - - if (_propertyValues.TryGetValue(property, out var value)) + if (_values.TryGetValue(property, out var slot)) { - result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; - } + if (slot is PriorityValue p) + { + p.ClearLocalValue(); + } + else + { + var remove = slot is ConstantValueEntry c ? + c.Priority == BindingPriority.LocalValue : + !(slot is IPriorityValueEntry); - return result; + if (remove) + { + var old = TryGetValue(property, out var value) ? value : default; + _values.Remove(property); + _sink.ValueChanged( + property, + BindingPriority.LocalValue, + old, + BindingValue.Unset); + } + } + } } - 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 (priority == BindingPriority.LocalValue) + { + var old = (T)slot; + _values.SetValue(property, (object)value!); + _sink.ValueChanged(property, priority, old, value); + } + else + { + var existing = new ConstantValueEntry(property, (T)slot, BindingPriority.LocalValue); + var priorityValue = new PriorityValue(_owner, property, this, existing); + priorityValue.SetValue(value, priority); + _values.SetValue(property, priorityValue); } - - return new PriorityValue( - this, - property, - property.PropertyType, - validate2); } - private void Validate(AvaloniaProperty property, ref object value) + private IDisposable BindExisting( + 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 + { + var existing = new ConstantValueEntry(property, (T)slot, BindingPriority.LocalValue); + priorityValue = new PriorityValue(_owner, property, this, existing); } - 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..a217d02ecd3 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -201,8 +201,8 @@ public bool CanUserSortColumns public static readonly StyledProperty ColumnHeaderHeightProperty = AvaloniaProperty.Register( nameof(ColumnHeaderHeight), - defaultValue: double.NaN, - validate: ValidateColumnHeaderHeight); + defaultValue: double.NaN/*, + validate: ValidateColumnHeaderHeight*/); private static double ValidateColumnHeaderHeight(DataGrid grid, double value) { @@ -261,8 +261,8 @@ public IBrush AlternatingRowBackground public static readonly StyledProperty FrozenColumnCountProperty = AvaloniaProperty.Register( - nameof(FrozenColumnCount), - validate: ValidateFrozenColumnCount); + nameof(FrozenColumnCount)/*, + validate: ValidateFrozenColumnCount*/); /// /// Gets or sets the number of columns that the user cannot scroll horizontally. @@ -395,8 +395,8 @@ public bool IsValid public static readonly StyledProperty MaxColumnWidthProperty = AvaloniaProperty.Register( nameof(MaxColumnWidth), - defaultValue: DATAGRID_defaultMaxColumnWidth, - validate: ValidateMaxColumnWidth); + defaultValue: DATAGRID_defaultMaxColumnWidth/*, + validate: ValidateMaxColumnWidth*/); private static double ValidateMaxColumnWidth(DataGrid grid, double value) { @@ -433,8 +433,8 @@ public double MaxColumnWidth public static readonly StyledProperty MinColumnWidthProperty = AvaloniaProperty.Register( nameof(MinColumnWidth), - defaultValue: DATAGRID_defaultMinColumnWidth, - validate: ValidateMinColumnWidth); + defaultValue: DATAGRID_defaultMinColumnWidth/*, + validate: ValidateMinColumnWidth*/); private static double ValidateMinColumnWidth(DataGrid grid, double value) { @@ -482,8 +482,8 @@ public IBrush RowBackground public static readonly StyledProperty RowHeightProperty = AvaloniaProperty.Register( nameof(RowHeight), - defaultValue: double.NaN, - validate: ValidateRowHeight); + defaultValue: double.NaN/*, + validate: ValidateRowHeight*/); private static double ValidateRowHeight(DataGrid grid, double value) { if (value < DataGridRow.DATAGRIDROW_minimumHeight) @@ -510,8 +510,8 @@ public double RowHeight public static readonly StyledProperty RowHeaderWidthProperty = AvaloniaProperty.Register( nameof(RowHeaderWidth), - defaultValue: double.NaN, - validate: ValidateRowHeaderWidth); + defaultValue: double.NaN/*, + validate: ValidateRowHeaderWidth*/); private static double ValidateRowHeaderWidth(DataGrid grid, double value) { if (value < DATAGRID_minimumRowHeaderWidth) diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 7dafef9d8bb..bede7f481eb 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -67,8 +67,8 @@ public bool IsPropertyNameVisible public static readonly StyledProperty SublevelIndentProperty = AvaloniaProperty.Register( nameof(SublevelIndent), - defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent, - validate: ValidateSublevelIndent); + defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent/*, + validate: ValidateSublevelIndent*/); private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value) { diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 64db832a815..1e1a62f4a4a 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -377,8 +377,8 @@ public class AutoCompleteBox : TemplatedControl /// dependency property. public static readonly StyledProperty MinimumPrefixLengthProperty = AvaloniaProperty.Register( - nameof(MinimumPrefixLength), 1, - validate: ValidateMinimumPrefixLength); + nameof(MinimumPrefixLength), 1/*, + validate: ValidateMinimumPrefixLength*/); /// /// Identifies the @@ -391,8 +391,8 @@ public class AutoCompleteBox : TemplatedControl public static readonly StyledProperty MinimumPopulateDelayProperty = AvaloniaProperty.Register( nameof(MinimumPopulateDelay), - TimeSpan.Zero, - validate: ValidateMinimumPopulateDelay); + TimeSpan.Zero/*, + validate: ValidateMinimumPopulateDelay*/); /// /// Identifies the @@ -405,8 +405,8 @@ public class AutoCompleteBox : TemplatedControl public static readonly StyledProperty MaxDropDownHeightProperty = AvaloniaProperty.Register( nameof(MaxDropDownHeight), - double.PositiveInfinity, - validate: ValidateMaxDropDownHeight); + double.PositiveInfinity/*, + validate: ValidateMaxDropDownHeight*/); /// /// Identifies the @@ -494,8 +494,8 @@ public class AutoCompleteBox : TemplatedControl public static readonly StyledProperty FilterModeProperty = AvaloniaProperty.Register( nameof(FilterMode), - defaultValue: AutoCompleteFilterMode.StartsWith, - validate: ValidateFilterMode); + defaultValue: AutoCompleteFilterMode.StartsWith/*, + validate: ValidateFilterMode*/); /// /// Identifies the diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 78d02e200fa..d5e624ffe4b 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -306,18 +306,13 @@ 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) { @@ -328,6 +323,11 @@ protected override void UpdateDataValidation(AvaloniaProperty property, BindingN } } + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + IsPressed = false; + } + /// /// Called when the property changes. /// diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index beafab3edfd..58b7d7cb479 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -351,8 +351,8 @@ public IBrush HeaderBackground public static readonly StyledProperty DisplayModeProperty = AvaloniaProperty.Register( - nameof(DisplayMode), - validate: ValidateDisplayMode); + nameof(DisplayMode)/*, + validate: ValidateDisplayMode*/); /// /// Gets or sets a value indicating whether the calendar is displayed in /// months, years, or decades. diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 841b73cd92c..aa3a8fae3db 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -189,14 +189,14 @@ public class DatePicker : TemplatedControl public static readonly StyledProperty SelectedDateFormatProperty = AvaloniaProperty.Register( nameof(SelectedDateFormat), - defaultValue: DatePickerFormat.Short, - validate: ValidateSelectedDateFormat); + defaultValue: DatePickerFormat.Short/*, + validate: ValidateSelectedDateFormat*/); public static readonly StyledProperty CustomDateFormatStringProperty = AvaloniaProperty.Register( nameof(CustomDateFormatString), - defaultValue: "d", - validate: ValidateDateFormatString); + defaultValue: "d"/*, + validate: ValidateDateFormatString*/); 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); } } 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/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 6d450a01559..9bc97e3758a 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/*, validate: 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/*, validate: OnCoerceMaximum*/); /// /// Defines the property. /// public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum); + AvaloniaProperty.Register(nameof(Minimum), double.MinValue/*, validate: OnCoerceMinimum*/); /// /// Defines the property. 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 0e2136a6f3b..457d72bd600 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; @@ -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.ValueOrDefault(); + var newDataSource = newEnumerable as ItemsSourceView; + if (newEnumerable != null && newDataSource == null) { - newDataSource = new ItemsSourceView(newValue); + newDataSource = new ItemsSourceView(newEnumerable); } OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); } else if (property == ItemTemplateProperty) { - OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); + OnItemTemplateChanged(oldValue.ValueOrDefault(), newValue.ValueOrDefault()); } else if (property == LayoutProperty) { - OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); + OnLayoutChanged(oldValue.ValueOrDefault(), newValue.ValueOrDefault()); } else if (property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = (double)args.NewValue; + _viewportManager.HorizontalCacheLength = newValue.ValueOrDefault(); } else if (property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = (double)args.NewValue; - } - else - { - base.OnPropertyChanged(args); + _viewportManager.VerticalCacheLength = newValue.ValueOrDefault(); } + + 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 3d472fca18c..becc4c8a612 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); @@ -700,11 +700,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.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e9735b9b31c..e95a87dd16d 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 { @@ -293,11 +294,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.ValueOrDefault(); //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 edc20429226..feaf98553da 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 { @@ -436,40 +437,42 @@ 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.ValueOrDefault(); //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //i.e. the properties are the inverse of each other. var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; _orientation.ScrollOrientation = scrollOrientation; } - else if (args.Property == MinColumnSpacingProperty) + else if (property == MinColumnSpacingProperty) { - _minColumnSpacing = (double)args.NewValue; + _minColumnSpacing = newValue.ValueOrDefault(); } - else if (args.Property == MinRowSpacingProperty) + else if (property == MinRowSpacingProperty) { - _minRowSpacing = (double)args.NewValue; + _minRowSpacing = newValue.ValueOrDefault(); } - else if (args.Property == ItemsJustificationProperty) + else if (property == ItemsJustificationProperty) { - _itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue; + _itemsJustification = newValue.ValueOrDefault(); + ; } - else if (args.Property == ItemsStretchProperty) + else if (property == ItemsStretchProperty) { - _itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue; + _itemsStretch = newValue.ValueOrDefault(); + ; } - else if (args.Property == MinItemWidthProperty) + else if (property == MinItemWidthProperty) { - _minItemWidth = (double)args.NewValue; + _minItemWidth = newValue.ValueOrDefault(); } - else if (args.Property == MinItemHeightProperty) + else if (property == MinItemHeightProperty) { - _minItemHeight = (double)args.NewValue; + _minItemHeight = newValue.ValueOrDefault(); } InvalidateLayout(); diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index de8093c0489..cc7da2b4369 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -478,7 +478,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/Visual.cs b/src/Avalonia.Visuals/Visual.cs index f4306d39298..d615bea450b 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -433,7 +433,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..3cf308f7be1 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() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 23984a7c8d6..38f47ab95a6 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,158 @@ 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 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 +268,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 +333,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 +444,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("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>(); - Assert.Equal(8.9, target.GetValue(Class1.QuxProperty)); + 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 +517,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 +528,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 +587,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 +618,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_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 deleted file mode 100644 index f0e93dbb3af..00000000000 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive.Subjects; -using Xunit; - -namespace Avalonia.Base.UnitTests -{ - public class AvaloniaObjectTests_Validation - { - [Fact] - public void SetValue_Causes_Validation() - { - var target = new Class1(); - - target.SetValue(Class1.QuxProperty, 5); - Assert.Throws(() => target.SetValue(Class1.QuxProperty, 25)); - Assert.Equal(5, target.GetValue(Class1.QuxProperty)); - } - - [Fact] - public void SetValue_Causes_Coercion() - { - var target = new Class1(); - - target.SetValue(Class1.QuxProperty, 5); - Assert.Equal(5, target.GetValue(Class1.QuxProperty)); - target.SetValue(Class1.QuxProperty, -5); - Assert.Equal(0, target.GetValue(Class1.QuxProperty)); - target.SetValue(Class1.QuxProperty, 15); - Assert.Equal(10, target.GetValue(Class1.QuxProperty)); - } - - [Fact] - public void Revalidate_Causes_Recoercion() - { - var target = new Class1(); - - target.SetValue(Class1.QuxProperty, 7); - Assert.Equal(7, target.GetValue(Class1.QuxProperty)); - target.MaxQux = 5; - target.Revalidate(Class1.QuxProperty); - } - - [Fact] - public void Validation_Can_Be_Overridden() - { - var target = new Class2(); - Assert.Throws(() => target.SetValue(Class1.QuxProperty, 5)); - } - - [Fact] - public void Validation_Can_Be_Overridden_With_Null() - { - var target = new Class3(); - target.SetValue(Class1.QuxProperty, 50); - Assert.Equal(50, target.GetValue(Class1.QuxProperty)); - } - - [Fact] - public void Binding_To_UnsetValue_Doesnt_Throw() - { - var target = new Class1(); - var source = new Subject(); - - target.Bind(Class1.QuxProperty, source); - - source.OnNext(AvaloniaProperty.UnsetValue); - } - - [Fact] - public void Attached_Property_Should_Be_Validated() - { - var target = new Class2(); - - target.SetValue(Class1.AttachedProperty, 15); - Assert.Equal(10, target.GetValue(Class1.AttachedProperty)); - } - - [Fact] - public void PropertyChanged_Event_Uses_Coerced_Value() - { - var inst = new Class1(); - inst.PropertyChanged += (sender, e) => - { - Assert.Equal(10, e.NewValue); - }; - - inst.SetValue(Class1.QuxProperty, 15); - } - - private class Class1 : AvaloniaObject - { - public static readonly StyledProperty QuxProperty = - AvaloniaProperty.Register("Qux", validate: Validate); - - public static readonly AttachedProperty AttachedProperty = - AvaloniaProperty.RegisterAttached("Attached", validate: Validate); - - public Class1() - { - MaxQux = 10; - ErrorQux = 20; - } - - public int MaxQux { get; set; } - - public int ErrorQux { get; } - - private static int Validate(Class1 instance, int value) - { - if (value > instance.ErrorQux) - { - throw new ArgumentOutOfRangeException(); - } - - return Math.Min(Math.Max(value, 0), ((Class1)instance).MaxQux); - } - - private static int Validate(Class2 instance, int value) - { - return Math.Min(value, 10); - } - } - - private class Class2 : AvaloniaObject - { - public static readonly StyledProperty 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..13c35814eeb 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -140,6 +140,37 @@ 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 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.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 7e053392c70..e29f4bfa9c6 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -780,7 +780,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..237ed58de5f 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs @@ -35,7 +35,7 @@ public void OneWay_Binding_Should_Be_Set_Up() target.Verify(x => x.Bind( TextBox.TextProperty, - It.IsAny>(), + It.IsAny>>(), BindingPriority.TemplatedParent)); } @@ -55,7 +55,7 @@ public void TwoWay_Binding_Should_Be_Set_Up() target.Verify(x => x.Bind( TextBox.TextProperty, - It.IsAny>(), + It.IsAny>>(), BindingPriority.TemplatedParent)); } @@ -68,7 +68,7 @@ private Mock CreateTarget( 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.Bind(It.IsAny(), 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 c6eeb1ec0e3..30a72535dca 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -131,7 +131,7 @@ public void SetValue(AvaloniaProperty property, T value, BindingPriority p throw new NotImplementedException(); } - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority) + public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority) { throw new NotImplementedException(); } @@ -146,7 +146,7 @@ public bool IsSet(AvaloniaProperty property) throw new NotImplementedException(); } - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { throw new NotImplementedException(); } @@ -165,6 +165,36 @@ public void NotifyResourcesChanged(ResourcesChangedEventArgs e) { throw new NotImplementedException(); } + + 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 class TestLogical1 : TestLogical diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index aef539becd1..56c308cdc7e 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -161,7 +161,7 @@ public void SetValue(AvaloniaProperty property, T value, BindingPriority p throw new NotImplementedException(); } - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { throw new NotImplementedException(); } @@ -176,7 +176,7 @@ public bool IsSet(AvaloniaProperty property) throw new NotImplementedException(); } - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { throw new NotImplementedException(); } @@ -195,6 +195,36 @@ public void NotifyResourcesChanged(ResourcesChangedEventArgs e) { throw new NotImplementedException(); } + + 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 class TestLogical1 : TestLogical diff --git a/tests/Avalonia.Styling.UnitTests/SetterTests.cs b/tests/Avalonia.Styling.UnitTests/SetterTests.cs index 52f75627f9a..4c8ea7753be 100644 --- a/tests/Avalonia.Styling.UnitTests/SetterTests.cs +++ b/tests/Avalonia.Styling.UnitTests/SetterTests.cs @@ -83,7 +83,7 @@ public void Setter_Should_Apply_Value_Without_Activator_With_Style_Priority() control.Verify(x => x.Bind( TextBlock.TextProperty, - It.IsAny>(), + It.IsAny>>(), BindingPriority.Style)); } @@ -99,7 +99,7 @@ public void Setter_Should_Apply_Value_With_Activator_With_StyleTrigger_Priority( control.Verify(x => x.Bind( TextBlock.TextProperty, - It.IsAny>(), + It.IsAny>>(), BindingPriority.StyleTrigger)); } @@ -114,7 +114,7 @@ public void Setter_Should_Apply_Binding_Without_Activator_With_Style_Priority() control.Verify(x => x.Bind( TextBlock.TextProperty, - It.IsAny>(), + It.IsAny>>(), BindingPriority.Style)); } @@ -130,7 +130,7 @@ public void Setter_Should_Apply_Binding_With_Activator_With_StyleTrigger_Priorit control.Verify(x => x.Bind( TextBlock.TextProperty, - It.IsAny>(), + It.IsAny>>(), BindingPriority.StyleTrigger)); } diff --git a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs index d9fabf6f5d1..89d6e6d5d30 100644 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs @@ -70,12 +70,42 @@ public bool IsSet(AvaloniaProperty property) throw new NotImplementedException(); } - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { throw new NotImplementedException(); } - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + 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(); } diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs index e92ac36e8f9..c15950454c7 100644 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs @@ -58,12 +58,12 @@ public void SetValue(AvaloniaProperty property, T value, BindingPriority p throw new NotImplementedException(); } - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { throw new NotImplementedException(); } - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { throw new NotImplementedException(); } @@ -77,5 +77,35 @@ public bool IsSet(AvaloniaProperty property) { throw new NotImplementedException(); } + + 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(); + } } } From 0cfa15913d29449ebfebb4b8c18231cf2ba492ff Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Nov 2019 15:30:52 +0100 Subject: [PATCH 03/50] Remove ignored validate parameter. --- src/Avalonia.Base/AvaloniaProperty.cs | 6 ++---- src/Avalonia.Controls/DefinitionBase.cs | 4 ++-- src/Avalonia.Controls/Grid.cs | 16 ++++++++-------- src/Avalonia.Controls/NativeMenu.Export.cs | 4 ++-- .../Notifications/NotificationCard.cs | 2 +- .../AvaloniaObjectTests_Attached.cs | 13 +------------ 6 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 1bda55285e1..a39b8e6e1df 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -302,8 +302,7 @@ public static AttachedProperty RegisterAttached( string name, TValue defaultValue = default(TValue), bool inherits = false, - BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null) + BindingMode defaultBindingMode = BindingMode.OneWay) where THost : IAvaloniaObject { Contract.Requires(name != null); @@ -335,8 +334,7 @@ public static AttachedProperty RegisterAttached( Type ownerType, TValue defaultValue = default(TValue), bool inherits = false, - BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null) + BindingMode defaultBindingMode = BindingMode.OneWay) where THost : IAvaloniaObject { Contract.Requires(name != null); diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index e4ae7774534..eae6cf5e305 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -750,8 +750,8 @@ private void OnLayoutUpdated(object sender, EventArgs e) /// public static readonly AttachedProperty SharedSizeGroupProperty = AvaloniaProperty.RegisterAttached( - "SharedSizeGroup", - validate: SharedSizeGroupPropertyValueValid); + "SharedSizeGroup"/*, + validate: SharedSizeGroupPropertyValueValid*/); /// /// Static ctor. Used for static registration of properties. diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 23c1cd47946..7b57288e760 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -2740,12 +2740,12 @@ private enum Flags public static readonly AttachedProperty ColumnProperty = AvaloniaProperty.RegisterAttached( "Column", - defaultValue: 0, + defaultValue: 0/*, validate: (_, v) => { if (v >= 0) return v; else throw new ArgumentException("Invalid Grid.Column value."); - }); + }*/); /// /// Row property. This is an attached property. @@ -2761,12 +2761,12 @@ private enum Flags public static readonly AttachedProperty RowProperty = AvaloniaProperty.RegisterAttached( "Row", - defaultValue: 0, + defaultValue: 0/*, validate: (_, v) => { if (v >= 0) return v; else throw new ArgumentException("Invalid Grid.Row value."); - }); + }*/); /// /// ColumnSpan property. This is an attached property. @@ -2781,12 +2781,12 @@ private enum Flags public static readonly AttachedProperty ColumnSpanProperty = AvaloniaProperty.RegisterAttached( "ColumnSpan", - defaultValue: 1, + defaultValue: 1/*, validate: (_, v) => { if (v >= 1) return v; else throw new ArgumentException("Invalid Grid.ColumnSpan value."); - }); + }*/); /// /// RowSpan property. This is an attached property. @@ -2801,12 +2801,12 @@ private enum Flags public static readonly AttachedProperty RowSpanProperty = AvaloniaProperty.RegisterAttached( "RowSpan", - defaultValue: 1, + defaultValue: 1/*, validate: (_, v) => { if (v >= 1) return v; else throw new ArgumentException("Invalid Grid.RowSpan value."); - }); + }*/); /// /// IsSharedSizeScope property marks scoping element for shared size. 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/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs index 3cf308f7be1..44e2976e038 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs @@ -38,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 From e4045445964da427ef507a2db3cd0062c5d4c85f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Nov 2019 16:09:47 +0100 Subject: [PATCH 04/50] Move check to avalonia properties. --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 5 ----- src/Avalonia.Base/DirectPropertyBase.cs | 17 ++++++++++------- src/Avalonia.Base/StyledPropertyBase.cs | 17 ++++++++++------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 4806abac4d7..0734b64721f 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -448,11 +448,6 @@ internal void NotifyInitialized(AvaloniaObject o) foreach (PropertyInitializationData data in initializationData) { - if (!data.Property.HasNotifyInitializedObservers) - { - continue; - } - data.Property.NotifyInitialized(o); } } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 0b3747a374a..a7d3f114bb9 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -104,13 +104,16 @@ public TValue GetUnsetValue(Type type) /// internal override void NotifyInitialized(IAvaloniaObject o) { - var e = new AvaloniaPropertyChangedEventArgs( - o, - this, - default, - InvokeGetter(o), - BindingPriority.Unset); - NotifyInitialized(e); + if (HasNotifyInitializedObservers) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + this, + default, + InvokeGetter(o), + BindingPriority.Unset); + NotifyInitialized(e); + } } /// diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index bbb47d63ad0..d842638e579 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -141,13 +141,16 @@ public override string ToString() /// internal override void NotifyInitialized(IAvaloniaObject o) { - var e = new AvaloniaPropertyChangedEventArgs( - o, - this, - default, - o.GetValue(this), - BindingPriority.Unset); - NotifyInitialized(e); + if (HasNotifyInitializedObservers) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + this, + default, + o.GetValue(this), + BindingPriority.Unset); + NotifyInitialized(e); + } } /// From b8717bf6dc433c6cacd66ac686d72a2abb1c50f1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Nov 2019 20:34:32 +0100 Subject: [PATCH 05/50] Tidy up API. --- src/Avalonia.Base/AvaloniaObject.cs | 56 +++++- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 189 +++++++++++++++++- src/Avalonia.Base/AvaloniaProperty`1.cs | 5 - src/Avalonia.Base/DirectPropertyBase.cs | 10 +- src/Avalonia.Base/IAvaloniaObject.cs | 48 ++--- src/Avalonia.Base/StyledPropertyBase.cs | 6 + .../AvaloniaPropertyTests.cs | 5 + .../Data/BindingTests_TemplatedParent.cs | 12 +- .../SelectorTests_Child.cs | 40 ++++ .../SelectorTests_Descendent.cs | 40 ++++ .../Avalonia.Styling.UnitTests/SetterTests.cs | 12 +- .../TestControlBase.cs | 40 ++++ .../TestTemplatedControl.cs | 40 ++++ 13 files changed, 437 insertions(+), 66 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 0ce468354cc..bcce2080a63 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -9,7 +9,6 @@ using Avalonia.Logging; using Avalonia.PropertyStore; using Avalonia.Threading; -using Avalonia.Utilities; namespace Avalonia { @@ -136,7 +135,8 @@ 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); } @@ -146,24 +146,47 @@ public void ClearValue(AvaloniaProperty property) /// The property. public void ClearValue(AvaloniaProperty property) { + property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); switch (property) { case StyledPropertyBase styled: - _values.ClearLocalValue(styled); + ClearValue(styled); break; case DirectPropertyBase direct: - var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, direct); - p.InvokeSetter(this, p.GetUnsetValue(GetType())); + ClearValue(direct); break; - case null: - throw new ArgumentNullException(nameof(property)); 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(); + + var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + p.InvokeSetter(this, p.GetUnsetValue(GetType())); + } + /// /// Compares two objects using reference equality. /// @@ -202,6 +225,8 @@ public void ClearValue(AvaloniaProperty property) /// The value. public object GetValue(AvaloniaProperty property) { + property = property ?? throw new ArgumentNullException(nameof(property)); + return property.RouteGetValue(this); } @@ -213,11 +238,12 @@ public object GetValue(AvaloniaProperty property) /// The value. public T GetValue(AvaloniaProperty property) { + property = property ?? throw new ArgumentNullException(nameof(property)); + return property switch { StyledPropertyBase styled => GetValue(styled), DirectPropertyBase direct => GetValue(direct), - null => throw new ArgumentNullException(nameof(property)), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") }; } @@ -292,6 +318,8 @@ public void SetValue( object value, BindingPriority priority = BindingPriority.LocalValue) { + property = property ?? throw new ArgumentNullException(nameof(property)); + property.RouteSetValue(this, value, priority); } @@ -307,6 +335,8 @@ public void SetValue( T value, BindingPriority priority = BindingPriority.LocalValue) { + property = property ?? throw new ArgumentNullException(nameof(property)); + switch (property) { case StyledPropertyBase styled: @@ -315,8 +345,6 @@ public void SetValue( case DirectPropertyBase direct: SetValue(direct, value); break; - case null: - throw new ArgumentNullException(nameof(property)); default: throw new NotSupportedException("Unsupported AvaloniaProperty type."); } @@ -386,6 +414,9 @@ public IDisposable Bind( IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + return property.RouteBind(this, source, priority); } @@ -404,11 +435,13 @@ public IDisposable Bind( IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + return property switch { StyledPropertyBase styled => Bind(styled, source, priority), DirectPropertyBase direct => Bind(direct, source), - null => throw new ArgumentNullException(nameof(property)), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), }; } @@ -429,6 +462,7 @@ public IDisposable Bind( BindingPriority priority = BindingPriority.LocalValue) { property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); return Values.AddBinding(property, source, priority); diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 6a513231d5d..a4c7fa95a50 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -125,7 +125,7 @@ public static IObservable> GetBindingObservable( /// for the specified property. /// public static IObservable GetPropertyChangedObservable( - this IAvaloniaObject o, + this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); @@ -236,6 +236,58 @@ public static ISubject> GetBindingSubject( 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. /// @@ -252,6 +304,10 @@ public static IDisposable Bind( 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(), @@ -274,6 +330,10 @@ public static IDisposable Bind( 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(), @@ -299,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) @@ -321,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`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 566a1531359..be58ff796d5 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -43,11 +43,6 @@ protected AvaloniaProperty( { } - internal override void RouteClearValue(IAvaloniaObject o) - { - o.ClearValue(this); - } - protected BindingValue TryConvert(object value) { if (value == UnsetValue) diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index a7d3f114bb9..7a0be065eb6 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -116,6 +116,12 @@ internal override void NotifyInitialized(IAvaloniaObject o) } } + /// + internal override void RouteClearValue(IAvaloniaObject o) + { + o.ClearValue(this); + } + /// internal override object? RouteGetValue(IAvaloniaObject o) { @@ -132,7 +138,7 @@ internal override void RouteSetValue( if (v.HasValue) { - o.SetValue(this, (TValue)v.Value, priority); + o.SetValue(this, (TValue)v.Value); } else if (v.Type == BindingValueType.UnsetValue) { @@ -151,7 +157,7 @@ internal override IDisposable RouteBind( BindingPriority priority) { var adapter = TypedBindingAdapter.Create(o, this, source); - return o.Bind(this, adapter, priority); + return o.Bind(this, adapter); } internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent) diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 066f9121eb0..4fec3d71afe 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -17,23 +17,24 @@ public interface IAvaloniaObject event EventHandler PropertyChanged; /// - /// Clears a 's local value. + /// Clears an 's local value. /// /// The property. - public void ClearValue(AvaloniaProperty property); + void ClearValue(StyledPropertyBase property); /// - /// Clears a 's local value. + /// Clears an 's local value. /// /// The property. - public void ClearValue(AvaloniaProperty 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. @@ -41,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. @@ -60,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); /// @@ -74,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); /// @@ -100,14 +99,12 @@ 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( - AvaloniaProperty property, - IObservable> source, - BindingPriority priority = BindingPriority.LocalValue); + DirectPropertyBase property, + IObservable> source); /// /// Registers an object as an inheritance child. @@ -130,19 +127,14 @@ IDisposable Bind( /// void RemoveInheritanceChild(IAvaloniaObject child); - //void InheritanceParentChanged( - // StyledPropertyBase property, - // IAvaloniaObject oldParent, - // IAvaloniaObject newParent); - /// /// Called when an inheritable property changes on an object registered as an inheritance /// parent. /// /// The type of the value. /// The property that has changed. - /// The old property value. - /// The new property value. + /// + /// void InheritedPropertyChanged( AvaloniaProperty property, Optional oldValue, diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index d842638e579..129b1f3c126 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -153,6 +153,12 @@ internal override void NotifyInitialized(IAvaloniaObject o) } } + /// + internal override void RouteClearValue(IAvaloniaObject o) + { + o.ClearValue(this); + } + /// internal override object RouteGetValue(IAvaloniaObject o) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 13c35814eeb..90b8bcff63d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -154,6 +154,11 @@ internal override IDisposable RouteBind( throw new NotImplementedException(); } + internal override void RouteClearValue(IAvaloniaObject o) + { + throw new NotImplementedException(); + } + internal override object RouteGetValue(IAvaloniaObject o) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs index 237ed58de5f..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.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index 30a72535dca..6e9447fff10 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -195,6 +195,46 @@ public void InheritedPropertyChanged(AvaloniaProperty property, Optional(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 56c308cdc7e..f64a3010c43 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -225,6 +225,46 @@ public void InheritedPropertyChanged(AvaloniaProperty property, Optional(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 4c8ea7753be..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.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs index 89d6e6d5d30..4bc7baa4dbc 100644 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs @@ -109,5 +109,45 @@ public void InheritedPropertyChanged(AvaloniaProperty property, Optional(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(); + } } } diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs index c15950454c7..58d1a57bdf8 100644 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs @@ -107,5 +107,45 @@ public void InheritedPropertyChanged(AvaloniaProperty property, Optional(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(); + } } } From aa3dfcd426a85e5b4f3ddf659eff80712dcf545d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Nov 2019 00:16:10 +0100 Subject: [PATCH 06/50] Remove DeferredSetter. It's not needed any more. --- src/Avalonia.Base/Utilities/DeferredSetter.cs | 128 ------------------ 1 file changed, 128 deletions(-) delete mode 100644 src/Avalonia.Base/Utilities/DeferredSetter.cs 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); - } -} From 49b91d129de13a5f2556defa226f297dc70d8626 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Nov 2019 00:17:18 +0100 Subject: [PATCH 07/50] Fix stack overflow. Don't believe the new value given to us in the `AvaloniaPropertyChangedEventArgs`: it may have already changed. Instead, read the current value of the property from the object. --- .../Reactive/AvaloniaPropertyBindingObservable.cs | 11 +++++++++-- .../Reactive/AvaloniaPropertyObservable.cs | 9 +++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs index 1bb4d917d24..d157a00fee3 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Data; #nullable enable @@ -47,8 +48,14 @@ private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _property) { - _value = (T)e.NewValue; - PublishNext(new BindingValue(_value)); + var typedArgs = (AvaloniaPropertyChangedEventArgs)e; + var newValue = e.Sender.GetValue(typedArgs.Property); + + if (!typedArgs.OldValue.HasValue || !EqualityComparer.Default.Equals(newValue, _value)) + { + _value = newValue; + PublishNext(_value); + } } } } diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index 4385ab13ef3..100330ed1d3 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -44,8 +44,13 @@ private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _property) { - _value = (T)e.NewValue; - PublishNext(_value); + var newValue = e.Sender.GetValue(e.Property); + + if (!Equals(newValue, _value)) + { + _value = (T)newValue; + PublishNext(_value); + } } } } From aa9386ddaf3df035a2fd12f0d084ec857d3f4e3d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Nov 2019 09:30:46 +0100 Subject: [PATCH 08/50] Add another sanity check to BindingValue. --- src/Avalonia.Base/Data/BindingValue.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 8265a6cd539..50e71cf3fc7 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -344,6 +344,11 @@ private static void ValidateValue(T value) { 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<>."); + } } } From 60422d621ba8cb0023730f745224b0b1d55b8916 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Nov 2019 09:31:07 +0100 Subject: [PATCH 09/50] Handle GetBindingObservable(). --- .../AvaloniaPropertyBindingObservable.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs index d157a00fee3..95016406268 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -48,13 +48,25 @@ private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _property) { - var typedArgs = (AvaloniaPropertyChangedEventArgs)e; - var newValue = e.Sender.GetValue(typedArgs.Property); + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + var newValue = e.Sender.GetValue(typedArgs.Property); - if (!typedArgs.OldValue.HasValue || !EqualityComparer.Default.Equals(newValue, _value)) + if (!typedArgs.OldValue.HasValue || !EqualityComparer.Default.Equals(newValue, _value)) + { + _value = newValue; + PublishNext(_value); + } + } + else { - _value = newValue; - PublishNext(_value); + var newValue = e.Sender.GetValue(e.Property); + + if (!Equals(newValue, _value)) + { + _value = (T)newValue; + PublishNext(_value); + } } } } From 8ed800ad715ac9bbce8e36804a6e5c3fd9474e7a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 16 Nov 2019 12:19:37 +0100 Subject: [PATCH 10/50] I did the merge wrong. Delete some files that should have been deleted in merge. --- .../TestControlBase.cs | 153 ------------------ .../TestTemplatedControl.cs | 151 ----------------- 2 files changed, 304 deletions(-) delete mode 100644 tests/Avalonia.Styling.UnitTests/TestControlBase.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs diff --git a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs deleted file mode 100644 index 4bc7baa4dbc..00000000000 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Data; - -namespace Avalonia.Styling.UnitTests -{ - public class TestControlBase : IStyleable - { - public TestControlBase() - { - Classes = new Classes(); - SubscribeCheckObservable = new TestObservable(); - } - -#pragma warning disable CS0067 // Event not used - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; -#pragma warning restore CS0067 - - public string Name { get; set; } - - public virtual Classes Classes { get; set; } - - public Type StyleKey => GetType(); - - public TestObservable SubscribeCheckObservable { get; private set; } - - public ITemplatedControl TemplatedParent - { - get; - set; - } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - IObservable IStyleable.StyleDetach { get; } - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - 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(); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs deleted file mode 100644 index 58d1a57bdf8..00000000000 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Data; - -namespace Avalonia.Styling.UnitTests -{ - public abstract class TestTemplatedControl : ITemplatedControl, IStyleable - { - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; - - public abstract Classes Classes - { - get; - } - - public abstract string Name - { - get; - } - - public abstract Type StyleKey - { - get; - } - - public abstract ITemplatedControl TemplatedParent - { - get; - } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - IObservable IStyleable.StyleDetach { get; } - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - 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(); - } - } -} From 13431044c167748cd2d37c4b017999305ccd2d04 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 17 Nov 2019 07:04:21 +0100 Subject: [PATCH 11/50] Remove TryGetValueUntyped. --- src/Avalonia.Base/ValueStore.cs | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index f6f489ef2c2..c3309bf52c6 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -34,38 +34,27 @@ public bool IsAnimating(AvaloniaProperty property) } public bool IsSet(AvaloniaProperty property) - { - return TryGetValueUntyped(property, out _); - } - - public bool TryGetValue(StyledPropertyBase property, out T value) { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue v) + if (slot is IValue v) { - if (v.Value.HasValue) - { - value = v.Value.Value; - return true; - } + return v.Value.HasValue; } else { - value = (T)slot; return true; } } - value = default!; return false; } - public bool TryGetValueUntyped(AvaloniaProperty property, out object? value) + public bool TryGetValue(StyledPropertyBase property, out T value) { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue v) + if (slot is IValue v) { if (v.Value.HasValue) { @@ -75,12 +64,12 @@ public bool TryGetValueUntyped(AvaloniaProperty property, out object? value) } else { - value = slot; + value = (T)slot; return true; } } - value = default; + value = default!; return false; } From 8b1efc53ca99361837a8da7580e960de645db51a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Nov 2019 11:56:46 +0100 Subject: [PATCH 12/50] Make Optional readonly. --- src/Avalonia.Base/Data/Optional.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index de7d6a307d9..80abd906464 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -20,7 +20,7 @@ namespace Avalonia.Data /// conversion from /// - For an missing value, use or simply `default` /// - public struct Optional + public readonly struct Optional { private readonly T _value; From 55eea624eee3ca8cf3a5abfc13a2804f07022cac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Nov 2019 11:58:11 +0100 Subject: [PATCH 13/50] Implement IEquatable on Optional. --- src/Avalonia.Base/Data/Optional.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index 80abd906464..3fbcd356ff1 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -20,7 +20,7 @@ namespace Avalonia.Data /// conversion from /// - For an missing value, use or simply `default` /// - public readonly struct Optional + public readonly struct Optional : IEquatable> { private readonly T _value; @@ -50,6 +50,9 @@ public Optional(T 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; From a29a2db61626533fda2bbc69c713cd56e9c87503 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Nov 2019 11:58:39 +0100 Subject: [PATCH 14/50] Removed unused method. --- src/Avalonia.Base/DirectProperty.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 30eb9cf69c0..2a8c7316141 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -201,15 +201,5 @@ void IDirectPropertyAccessor.SetValue(IAvaloniaObject instance, object value) Setter((TOwner)instance, (TValue)value); } - - internal void WrapSetter(TOwner instance, BindingValue value) - { - if (Setter == null) - { - throw new ArgumentException($"The property {Name} is readonly."); - } - - Setter(instance, value.Value); - } } } From 557898adfdd68ee9cc6eebec432de71b8316c1ab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Nov 2019 11:59:57 +0100 Subject: [PATCH 15/50] Move methods back to original order. Moving them like this was confusing the diff. --- src/Avalonia.Controls/Button.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index d5e624ffe4b..2e115463ac8 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -307,6 +307,11 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + IsPressed = false; + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { base.UpdateDataValidation(property, value); @@ -323,11 +328,6 @@ protected override void UpdateDataValidation(AvaloniaProperty property, Bi } } - protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) - { - IsPressed = false; - } - /// /// Called when the property changes. /// From 44e1bd5158c8593f5bb896f5c85c043f5aad8c19 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Nov 2019 18:15:57 +0100 Subject: [PATCH 16/50] Use LocalValueEntry instead of boxing. Further reduces allocated memory in the common case of only a local value being set. --- .../PropertyStore/LocalValueEntry.cs | 19 +++++++++ src/Avalonia.Base/ValueStore.cs | 42 ++++++++++--------- 2 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 src/Avalonia.Base/PropertyStore/LocalValueEntry.cs diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs new file mode 100644 index 00000000000..416185df057 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -0,0 +1,19 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + internal class LocalValueEntry : IValue + { + public LocalValueEntry(T value) => Value = value; + public Optional Value { get; set; } + public BindingPriority ValuePriority => BindingPriority.LocalValue; + Optional IValue.Value => Value.ToObject(); + + public ConstantValueEntry ToConstantValueEntry(StyledPropertyBase property) + { + return new ConstantValueEntry(property, Value.Value, BindingPriority.LocalValue); + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index c3309bf52c6..ddcf7ee9c19 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -41,10 +41,6 @@ public bool IsSet(AvaloniaProperty property) { return v.Value.HasValue; } - else - { - return true; - } } return false; @@ -62,11 +58,6 @@ public bool TryGetValue(StyledPropertyBase property, out T value) return true; } } - else - { - value = (T)slot; - return true; - } } value = default!; @@ -81,7 +72,7 @@ public void SetValue(StyledPropertyBase property, T value, BindingPriority } else if (priority == BindingPriority.LocalValue) { - _values.AddValue(property, (object)value!); + _values.AddValue(property, new LocalValueEntry(value)); _sink.ValueChanged(property, priority, default, value); } else @@ -174,18 +165,25 @@ private void SetExisting( { p.SetValue(value, priority); } - else if (priority == BindingPriority.LocalValue) + else if (slot is LocalValueEntry l) { - var old = (T)slot; - _values.SetValue(property, (object)value!); - _sink.ValueChanged(property, priority, old, value); + if (priority == BindingPriority.LocalValue) + { + var old = l.Value; + l.Value = value; + _sink.ValueChanged(property, priority, old, value); + } + else + { + var existing = l.ToConstantValueEntry(property); + var priorityValue = new PriorityValue(_owner, property, this, existing); + priorityValue.SetValue(value, priority); + _values.SetValue(property, priorityValue); + } } else { - var existing = new ConstantValueEntry(property, (T)slot, BindingPriority.LocalValue); - var priorityValue = new PriorityValue(_owner, property, this, existing); - priorityValue.SetValue(value, priority); - _values.SetValue(property, priorityValue); + throw new NotSupportedException("Unrecognised value store slot type."); } } @@ -205,11 +203,15 @@ private IDisposable BindExisting( { priorityValue = p; } - else + else if (slot is LocalValueEntry l) { - var existing = new ConstantValueEntry(property, (T)slot, BindingPriority.LocalValue); + var existing = l.ToConstantValueEntry(property); priorityValue = new PriorityValue(_owner, property, this, existing); } + else + { + throw new NotSupportedException("Unrecognised value store slot type."); + } var binding = priorityValue.AddBinding(source, priority); _values.SetValue(property, priorityValue); From d5dc4704164c6fddd5465e827fcdacbb310c4703 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Nov 2019 18:58:14 +0100 Subject: [PATCH 17/50] Fixed issue with LocalValue and bindings. --- .../PropertyStore/LocalValueEntry.cs | 12 ++-- .../PropertyStore/PriorityValue.cs | 56 +++++++++++++------ src/Avalonia.Base/ValueStore.cs | 9 +-- .../AvaloniaObjectTests_Binding.cs | 31 ++++++++++ 4 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 416185df057..067ed7b9663 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -6,14 +6,12 @@ namespace Avalonia.PropertyStore { internal class LocalValueEntry : IValue { - public LocalValueEntry(T value) => Value = value; - public Optional Value { get; set; } + private T _value; + + public LocalValueEntry(T value) => _value = value; + public Optional Value => _value; public BindingPriority ValuePriority => BindingPriority.LocalValue; Optional IValue.Value => Value.ToObject(); - - public ConstantValueEntry ToConstantValueEntry(StyledPropertyBase property) - { - return new ConstantValueEntry(property, Value.Value, BindingPriority.LocalValue); - } + public void SetValue(T value) => _value = value; } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index e33ab48353c..1a4b616bd9a 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -40,6 +40,18 @@ public PriorityValue( } } + 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; } @@ -77,7 +89,11 @@ void IValueSink.ValueChanged( Optional oldValue, BindingValue newValue) { - _localValue = default; + if (priority == BindingPriority.LocalValue) + { + _localValue = default; + } + UpdateEffectiveValue(); } @@ -108,29 +124,37 @@ private void UpdateEffectiveValue() var reachedLocalValues = false; var value = default(Optional); - for (var i = _entries.Count - 1; i >= 0; --i) + if (_entries.Count > 0) { - var entry = _entries[i]; - - if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) + for (var i = _entries.Count - 1; i >= 0; --i) { - reachedLocalValues = true; + var entry = _entries[i]; - if (_localValue.HasValue) + if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) { - value = _localValue; - ValuePriority = BindingPriority.LocalValue; - break; + reachedLocalValues = true; + + if (_localValue.HasValue) + { + value = _localValue; + ValuePriority = BindingPriority.LocalValue; + break; + } } - } - if (entry.Value.HasValue) - { - value = entry.Value; - ValuePriority = entry.Priority; - break; + if (entry.Value.HasValue) + { + value = entry.Value; + ValuePriority = entry.Priority; + break; + } } } + else if (_localValue.HasValue) + { + value = _localValue; + ValuePriority = BindingPriority.LocalValue; + } if (value != Value) { diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index ddcf7ee9c19..b63692a03b2 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -170,14 +170,12 @@ private void SetExisting( if (priority == BindingPriority.LocalValue) { var old = l.Value; - l.Value = value; + l.SetValue(value); _sink.ValueChanged(property, priority, old, value); } else { - var existing = l.ToConstantValueEntry(property); - var priorityValue = new PriorityValue(_owner, property, this, existing); - priorityValue.SetValue(value, priority); + var priorityValue = new PriorityValue(_owner, property, this, l); _values.SetValue(property, priorityValue); } } @@ -205,8 +203,7 @@ private IDisposable BindExisting( } else if (slot is LocalValueEntry l) { - var existing = l.ToConstantValueEntry(property); - priorityValue = new PriorityValue(_owner, property, this, existing); + priorityValue = new PriorityValue(_owner, property, this, l); } else { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 38f47ab95a6..4c00d2a1ea6 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -101,6 +101,37 @@ public void Completing_LocalValue_Binding_Reverts_To_Default_Value_Even_When_Loc 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() { From 6ddc953103e49653752b438390ec7b09e38adbbc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Nov 2019 19:39:41 +0100 Subject: [PATCH 18/50] Implement diagnostic API. Doesn't actually fill in the "details" property for styled properties because this API is going to change shortly with the new devtools. --- src/Avalonia.Base/AvaloniaObject.cs | 27 ++++++++++++++++ .../Diagnostics/AvaloniaObjectExtensions.cs | 31 +------------------ src/Avalonia.Base/ValueStore.cs | 17 ++++++++++ 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index bcce2080a63..e6d54727b35 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -587,6 +587,33 @@ internal void InheritanceParentChanged( } } + internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property) + { + if (property.IsDirect) + { + return new AvaloniaPropertyValue( + property, + GetValue(property), + BindingPriority.Unset, + "Local Value"); + } + else if (_values != null) + { + var result = _values.GetDiagnostic(property); + + if (result != null) + { + return result; + } + } + + return new AvaloniaPropertyValue( + property, + GetValue(property), + BindingPriority.Unset, + "Unset"); + } + /// /// Logs a binding error for a property. /// diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index 4885f77d9c4..d062856a739 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs @@ -22,36 +22,7 @@ public static class AvaloniaObjectExtensions /// public static AvaloniaPropertyValue GetDiagnostic(this AvaloniaObject o, AvaloniaProperty property) { - throw new NotImplementedException(); - ////var set = o.GetSetValues(); - - ////if (set.TryGetValue(property, out var obj)) - ////{ - //// if (obj is PriorityValue value) - //// { - //// return new AvaloniaPropertyValue( - //// property, - //// o.GetValue(property), - //// (BindingPriority)value.ValuePriority, - //// value.GetDiagnostic()); - //// } - //// else - //// { - //// return new AvaloniaPropertyValue( - //// property, - //// obj, - //// BindingPriority.LocalValue, - //// "Local value"); - //// } - ////} - ////else - ////{ - //// return new AvaloniaPropertyValue( - //// property, - //// o.GetValue(property), - //// BindingPriority.Unset, - //// "Unset"); - ////} + return o.GetDiagnosticInternal(property); } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index b63692a03b2..44ee1c4c8fc 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -129,6 +129,23 @@ public void ClearLocalValue(StyledPropertyBase property) } } + public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) + { + if (_values.TryGetValue(property, out var slot)) + { + if (slot is IValue value) + { + return new Diagnostics.AvaloniaPropertyValue( + property, + value.Value.HasValue ? (object)value.Value : AvaloniaProperty.UnsetValue, + value.ValuePriority, + null); + } + } + + return null; + } + void IValueSink.ValueChanged( StyledPropertyBase property, BindingPriority priority, From aa81db75a0912ee4e95f4547d1218acc17f02cc6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Nov 2019 11:09:39 +0100 Subject: [PATCH 19/50] Added WPF-style validation for AvaloniaProperty. --- src/Avalonia.Base/AvaloniaProperty.cs | 7 +- src/Avalonia.Base/BoxedValue.cs | 28 ---- .../PropertyStore/BindingEntry.cs | 5 + src/Avalonia.Base/StyledProperty.cs | 4 +- src/Avalonia.Base/StyledPropertyBase.cs | 28 +++- src/Avalonia.Base/StyledPropertyMetadata`1.cs | 14 +- src/Avalonia.Base/ValueStore.cs | 5 + src/Avalonia.Controls.DataGrid/DataGrid.cs | 121 ++++-------------- .../DataGridRowGroupHeader.cs | 22 +--- src/Avalonia.Controls/AutoCompleteBox.cs | 43 ++----- src/Avalonia.Controls/Calendar/Calendar.cs | 16 +-- src/Avalonia.Controls/Calendar/DatePicker.cs | 30 +---- src/Avalonia.Controls/DefinitionBase.cs | 10 +- src/Avalonia.Controls/Grid.cs | 32 ++--- .../AvaloniaObjectTests_Validation.cs | 82 ++++++++++++ 15 files changed, 199 insertions(+), 248 deletions(-) delete mode 100644 src/Avalonia.Base/BoxedValue.cs create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index a39b8e6e1df..31a7f21a1b9 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -257,6 +257,7 @@ protected AvaloniaProperty( /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A value validation 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 @@ -268,6 +269,7 @@ public static StyledProperty Register( TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, + Func validate = null, Action notifying = null) where TOwner : IAvaloniaObject { @@ -282,6 +284,7 @@ public static StyledProperty Register( typeof(TOwner), metadata, inherits, + validate, notifying); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; @@ -297,12 +300,14 @@ public static StyledProperty Register( /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A value validation callback. /// A public static AttachedProperty RegisterAttached( string name, TValue defaultValue = default(TValue), bool inherits = false, - BindingMode defaultBindingMode = BindingMode.OneWay) + BindingMode defaultBindingMode = BindingMode.OneWay, + Func validate = null) where THost : IAvaloniaObject { Contract.Requires(name != null); 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/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 2a7cf098a56..a53dda2aefe 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -77,6 +77,11 @@ public void Start() 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; 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 129b1f3c126..8e7bfd64671 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using System.Diagnostics; using Avalonia.Data; using Avalonia.Reactive; @@ -23,12 +22,14 @@ public abstract class StyledPropertyBase : AvaloniaProperty, ISt /// 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) { @@ -41,6 +42,13 @@ protected StyledPropertyBase( } _inherits = inherits; + ValidateValue = validate; + + if (validate?.Invoke(metadata.DefaultValue) == false) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); + } } /// @@ -62,6 +70,11 @@ protected StyledPropertyBase(StyledPropertyBase source, Type ownerType) /// public override bool Inherits => _inherits; + /// + /// Gets the value validation callback for the property. + /// + public Func ValidateValue { get; } + /// /// Gets the default value for the property on the specified type. /// @@ -71,7 +84,7 @@ public TValue GetDefaultValue(Type type) { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Typed; + return GetMetadata(type).DefaultValue; } /// @@ -123,6 +136,15 @@ 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}'."); + } + } + base.OverrideMetadata(type, metadata); } @@ -209,7 +231,7 @@ 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 d4ce137e0a4..50236e2c7c2 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -12,25 +12,27 @@ namespace Avalonia /// public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata { + private Optional _defaultValue; + /// /// Initializes a new instance of the class. /// /// The default value of the property. /// The default binding mode. public StyledPropertyMetadata( - TValue defaultValue = default, + Optional defaultValue = default, BindingMode defaultBindingMode = BindingMode.Default) : base(defaultBindingMode) { - DefaultValue = new BoxedValue(defaultValue); + _defaultValue = defaultValue; } /// /// Gets the default value for the property. /// - internal BoxedValue DefaultValue { get; private set; } + internal TValue DefaultValue => _defaultValue.ValueOrDefault(); - object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed; + object IStyledPropertyMetadata.DefaultValue => DefaultValue; /// public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property) @@ -39,9 +41,9 @@ 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; } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 44ee1c4c8fc..09961b399b1 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -66,6 +66,11 @@ public bool TryGetValue(StyledPropertyBase property, out T value) public void SetValue(StyledPropertyBase property, T value, BindingPriority priority) { + if (property.ValidateValue?.Invoke(value) == false) + { + throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); + } + if (_values.TryGetValue(property, out var slot)) { SetExisting(slot, property, value, priority); diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index a217d02ecd3..86133d5fdb2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -201,21 +201,13 @@ public bool CanUserSortColumns public static readonly StyledProperty ColumnHeaderHeightProperty = AvaloniaProperty.Register( nameof(ColumnHeaderHeight), - defaultValue: double.NaN/*, - validate: ValidateColumnHeaderHeight*/); + defaultValue: double.NaN, + 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); } /// @@ -261,8 +253,8 @@ public IBrush AlternatingRowBackground public static readonly StyledProperty FrozenColumnCountProperty = AvaloniaProperty.Register( - nameof(FrozenColumnCount)/*, - validate: ValidateFrozenColumnCount*/); + nameof(FrozenColumnCount), + validate: ValidateFrozenColumnCount); /// /// Gets or sets the number of columns that the user cannot scroll horizontally. @@ -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)); @@ -395,30 +379,12 @@ public bool IsValid public static readonly StyledProperty MaxColumnWidthProperty = AvaloniaProperty.Register( nameof(MaxColumnWidth), - defaultValue: DATAGRID_defaultMaxColumnWidth/*, - validate: ValidateMaxColumnWidth*/); + defaultValue: DATAGRID_defaultMaxColumnWidth, + 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; } /// @@ -433,29 +399,12 @@ public double MaxColumnWidth public static readonly StyledProperty MinColumnWidthProperty = AvaloniaProperty.Register( nameof(MinColumnWidth), - defaultValue: DATAGRID_defaultMinColumnWidth/*, - validate: ValidateMinColumnWidth*/); + defaultValue: DATAGRID_defaultMinColumnWidth, + 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; } /// @@ -482,20 +431,13 @@ public IBrush RowBackground public static readonly StyledProperty RowHeightProperty = AvaloniaProperty.Register( nameof(RowHeight), - defaultValue: double.NaN/*, - validate: ValidateRowHeight*/); - private static double ValidateRowHeight(DataGrid grid, double value) + defaultValue: double.NaN, + 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); } /// @@ -510,20 +452,13 @@ public double RowHeight public static readonly StyledProperty RowHeaderWidthProperty = AvaloniaProperty.Register( nameof(RowHeaderWidth), - defaultValue: double.NaN/*, - validate: ValidateRowHeaderWidth*/); - private static double ValidateRowHeaderWidth(DataGrid grid, double value) + defaultValue: double.NaN, + 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 bede7f481eb..69dfed761fa 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -67,26 +67,12 @@ public bool IsPropertyNameVisible public static readonly StyledProperty SublevelIndentProperty = AvaloniaProperty.Register( nameof(SublevelIndent), - defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent/*, - validate: ValidateSublevelIndent*/); + defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent, + 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/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 1e1a62f4a4a..6deddef0d08 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -377,8 +377,8 @@ public class AutoCompleteBox : TemplatedControl /// dependency property. public static readonly StyledProperty MinimumPrefixLengthProperty = AvaloniaProperty.Register( - nameof(MinimumPrefixLength), 1/*, - validate: ValidateMinimumPrefixLength*/); + nameof(MinimumPrefixLength), 1, + validate: IsValidMinimumPrefixLength); /// /// Identifies the @@ -391,8 +391,8 @@ public class AutoCompleteBox : TemplatedControl public static readonly StyledProperty MinimumPopulateDelayProperty = AvaloniaProperty.Register( nameof(MinimumPopulateDelay), - TimeSpan.Zero/*, - validate: ValidateMinimumPopulateDelay*/); + TimeSpan.Zero, + validate: IsValidMinimumPopulateDelay); /// /// Identifies the @@ -405,8 +405,8 @@ public class AutoCompleteBox : TemplatedControl public static readonly StyledProperty MaxDropDownHeightProperty = AvaloniaProperty.Register( nameof(MaxDropDownHeight), - double.PositiveInfinity/*, - validate: ValidateMaxDropDownHeight*/); + double.PositiveInfinity, + validate: IsValidMaxDropDownHeight); /// /// Identifies the @@ -494,8 +494,8 @@ public class AutoCompleteBox : TemplatedControl public static readonly StyledProperty FilterModeProperty = AvaloniaProperty.Register( nameof(FilterMode), - defaultValue: AutoCompleteFilterMode.StartsWith/*, - validate: ValidateFilterMode*/); + defaultValue: AutoCompleteFilterMode.StartsWith, + 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/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 58b7d7cb479..94f8ad41a0b 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -351,8 +351,9 @@ public IBrush HeaderBackground public static readonly StyledProperty DisplayModeProperty = AvaloniaProperty.Register( - nameof(DisplayMode)/*, - validate: ValidateDisplayMode*/); + nameof(DisplayMode), + 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 aa3a8fae3db..b4d4fed9fc0 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -189,14 +189,14 @@ public class DatePicker : TemplatedControl public static readonly StyledProperty SelectedDateFormatProperty = AvaloniaProperty.Register( nameof(SelectedDateFormat), - defaultValue: DatePickerFormat.Short/*, - validate: ValidateSelectedDateFormat*/); + defaultValue: DatePickerFormat.Short, + validate: IsValidSelectedDateFormat); public static readonly StyledProperty CustomDateFormatStringProperty = AvaloniaProperty.Register( nameof(CustomDateFormatString), - defaultValue: "d"/*, - validate: ValidateDateFormatString*/); + defaultValue: "d", + validate: IsValidDateFormatString); public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect( @@ -1146,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 eae6cf5e305..6f121afbef1 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -356,7 +356,7 @@ 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); @@ -380,11 +380,11 @@ private static string SharedSizeGroupPropertyValueValid(Control _, string value) if (i == id.Length) { - return value; + return true; } } - throw new ArgumentException("Invalid SharedSizeGroup string."); + return false; } /// @@ -750,8 +750,8 @@ private void OnLayoutUpdated(object sender, EventArgs e) /// public static readonly AttachedProperty SharedSizeGroupProperty = AvaloniaProperty.RegisterAttached( - "SharedSizeGroup"/*, - validate: SharedSizeGroupPropertyValueValid*/); + "SharedSizeGroup", + validate: SharedSizeGroupPropertyValueValid); /// /// Static ctor. Used for static registration of properties. diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 7b57288e760..1781067abbe 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -2740,12 +2740,8 @@ private enum Flags public static readonly AttachedProperty ColumnProperty = AvaloniaProperty.RegisterAttached( "Column", - defaultValue: 0/*, - validate: (_, v) => - { - if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Column value."); - }*/); + defaultValue: 0, + validate: v => v >= 0); /// /// Row property. This is an attached property. @@ -2761,12 +2757,8 @@ private enum Flags public static readonly AttachedProperty RowProperty = AvaloniaProperty.RegisterAttached( "Row", - defaultValue: 0/*, - validate: (_, v) => - { - if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Row value."); - }*/); + defaultValue: 0, + validate: v => v >= 0); /// /// ColumnSpan property. This is an attached property. @@ -2781,12 +2773,8 @@ private enum Flags public static readonly AttachedProperty ColumnSpanProperty = AvaloniaProperty.RegisterAttached( "ColumnSpan", - defaultValue: 1/*, - validate: (_, v) => - { - if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.ColumnSpan value."); - }*/); + defaultValue: 1, + validate: v => v >= 0); /// /// RowSpan property. This is an attached property. @@ -2801,12 +2789,8 @@ private enum Flags public static readonly AttachedProperty RowSpanProperty = AvaloniaProperty.RegisterAttached( "RowSpan", - defaultValue: 1/*, - validate: (_, v) => - { - if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.RowSpan value."); - }*/); + defaultValue: 1, + validate: v => v >= 0); /// /// IsSharedSizeScope property marks scoping element for shared size. diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs new file mode 100644 index 00000000000..70de7b449f5 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -0,0 +1,82 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Subjects; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_Validation + { + [Fact] + public void Registration_Throws_If_DefaultValue_Fails_Validation() + { + Assert.Throws(() => + new StyledProperty( + "BadDefault", + typeof(Class1), + new StyledPropertyMetadata(101), + validate: Class1.ValidateFoo)); + } + + [Fact] + public void Metadata_Override_Throws_If_DefaultValue_Fails_Validation() + { + Assert.Throws(() => Class1.FooProperty.OverrideDefaultValue(101)); + } + + [Fact] + public void SetValue_Throws_If_Fails_Validation() + { + var target = new Class1(); + + Assert.Throws(() => target.SetValue(Class1.FooProperty, 101)); + } + + [Fact] + public void Reverts_To_DefaultValue_If_Binding_Fails_Validation() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source); + source.OnNext(150); + + Assert.Equal(11, target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings() + { + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); + + target.Bind(Class1.FooProperty, source1); + target.Bind(Class1.FooProperty, source2); + source1.OnNext(42); + source2.OnNext(150); + + Assert.Equal(11, target.GetValue(Class1.FooProperty)); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Qux", + defaultValue: 11, + validate: ValidateFoo); + + public static bool ValidateFoo(int value) + { + return value < 100; + } + } + + private class Class2 : AvaloniaObject + { + } + } +} From 0f04f4d01afc489d30d7bbbb71a095951e8c7095 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Nov 2019 11:03:49 +0100 Subject: [PATCH 20/50] Add WPF-style property coercion. --- src/Avalonia.Base/AvaloniaObject.cs | 10 ++ src/Avalonia.Base/AvaloniaProperty.cs | 7 +- src/Avalonia.Base/IAvaloniaObject.cs | 7 + .../PropertyStore/PriorityValue.cs | 14 ++ src/Avalonia.Base/StyledPropertyBase.cs | 21 +++ src/Avalonia.Base/StyledPropertyMetadata`1.cs | 17 ++- src/Avalonia.Base/ValueStore.cs | 27 ++++ .../AvaloniaObjectTests_Coercion.cs | 139 ++++++++++++++++++ 8 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e6d54727b35..5bddf956162 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -504,6 +504,16 @@ public IDisposable Bind( return new DirectBindingSubscription(this, property, source); } + /// + /// Coerces the specified . + /// + /// The type of the property. + /// The property. + public void CoerceValue(StyledPropertyBase property) + { + _values?.CoerceValue(property); + } + /// void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) { diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 31a7f21a1b9..8e5716a5bf6 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -257,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 value validation callback. + /// 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 @@ -270,6 +271,7 @@ public static StyledProperty Register( bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null, + Func coerce = null, Action notifying = null) where TOwner : IAvaloniaObject { @@ -277,7 +279,8 @@ public static StyledProperty Register( var metadata = new StyledPropertyMetadata( defaultValue, - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); var result = new StyledProperty( name, diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 4fec3d71afe..1ccdaa8f0b0 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -106,6 +106,13 @@ 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. /// diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 1a4b616bd9a..60baf9d405d 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -11,6 +11,7 @@ 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( @@ -21,6 +22,12 @@ public PriorityValue( _owner = owner; Property = property; _sink = sink; + + if (property.HasCoercion) + { + var metadata = property.GetMetadata(owner.GetType()); + _coerceValue = metadata.CoerceValue; + } } public PriorityValue( @@ -83,6 +90,8 @@ public BindingEntry AddBinding(IObservable> source, BindingPr return binding; } + public void CoerceValue() => UpdateEffectiveValue(); + void IValueSink.ValueChanged( StyledPropertyBase property, BindingPriority priority, @@ -156,6 +165,11 @@ private void UpdateEffectiveValue() ValuePriority = BindingPriority.LocalValue; } + if (value.HasValue && _coerceValue != null) + { + value = _coerceValue(_owner, value.Value); + } + if (value != Value) { var old = Value; diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 8e7bfd64671..8c4d683ae00 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -43,6 +43,7 @@ protected StyledPropertyBase( _inherits = inherits; ValidateValue = validate; + HasCoercion |= metadata.CoerceValue != null; if (validate?.Invoke(metadata.DefaultValue) == false) { @@ -75,6 +76,24 @@ protected StyledPropertyBase(StyledPropertyBase source, Type ownerType) /// 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. /// @@ -145,6 +164,8 @@ public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) } } + HasCoercion |= metadata.CoerceValue != null; + base.OverrideMetadata(type, metadata); } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 50236e2c7c2..bd6c1257767 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -19,18 +19,26 @@ public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyM /// /// The default value of the property. /// The default binding mode. + /// A value coercion callback. public StyledPropertyMetadata( Optional defaultValue = default, - BindingMode defaultBindingMode = BindingMode.Default) + BindingMode defaultBindingMode = BindingMode.Default, + Func coerce = null) : base(defaultBindingMode) { _defaultValue = defaultValue; + CoerceValue = coerce; } /// /// Gets the default value for the property. /// - internal TValue DefaultValue => _defaultValue.ValueOrDefault(); + public TValue DefaultValue => _defaultValue.ValueOrDefault(); + + /// + /// Gets the value coercion callback, if any. + /// + public Func? CoerceValue { get; private set; } object IStyledPropertyMetadata.DefaultValue => DefaultValue; @@ -45,6 +53,11 @@ public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty prope { _defaultValue = src.DefaultValue; } + + if (CoerceValue == null) + { + CoerceValue = src.CoerceValue; + } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 09961b399b1..dec1f623869 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -75,6 +75,13 @@ public void SetValue(StyledPropertyBase property, T value, BindingPriority { 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)); @@ -97,6 +104,15 @@ public IDisposable AddBinding( { 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); @@ -134,6 +150,17 @@ public void ClearLocalValue(StyledPropertyBase property) } } + public void CoerceValue(StyledPropertyBase property) + { + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.CoerceValue(); + } + } + } + public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) { if (_values.TryGetValue(property, out var slot)) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs new file mode 100644 index 00000000000..8d8dbb03a22 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -0,0 +1,139 @@ +// 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_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 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; + } + } + } +} From 1616e76735444fc5a186a419d4794a1e9ef161cf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Nov 2019 11:28:56 +0100 Subject: [PATCH 21/50] Added benchmarks for property validation/coercion. --- .../Base/StyledPropertyBenchmark.cs | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs index 4b57776759d..b70ae19275e 100644 --- a/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs @@ -84,18 +84,64 @@ public void Bind_Int_Property_Multiple_Priorities() } } - class StyledClass : AvaloniaObject + [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() { - private int _intValue; + 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); + } } } } From 40657425c139723bbb3226ae2d29cfb2286350f2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Nov 2019 21:58:18 +0100 Subject: [PATCH 22/50] Reintroduce coercion to NumericUpDown. --- .../NumericUpDown/NumericUpDown.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 9bc97e3758a..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() From 8e9a1cce0410afc40c77be87a4152e819164761c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Dec 2019 11:25:53 +0100 Subject: [PATCH 23/50] Use _value field where possible. Allows more code to be inlined by not checking `HasValue` twice. --- src/Avalonia.Base/Data/BindingValue.cs | 10 +++++----- src/Avalonia.Base/Data/Optional.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 50e71cf3fc7..e1f53698993 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -135,10 +135,10 @@ private BindingValue(BindingValueType type, T value, Exception? error) /// Converts the binding value to an . /// /// - public Optional ToOptional() => HasValue ? new Optional(Value) : default; + public Optional ToOptional() => HasValue ? new Optional(_value) : default; /// - public override string ToString() => HasError ? $"Error: {Error!.Message}" : Value?.ToString() ?? "(null)"; + public override string ToString() => HasError ? $"Error: {Error!.Message}" : _value?.ToString() ?? "(null)"; /// /// Converts the value to untyped representation, using , @@ -152,7 +152,7 @@ private BindingValue(BindingValueType type, T value, Exception? error) { BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue, BindingValueType.DoNothing => BindingOperations.DoNothing, - BindingValueType.Value => Value, + BindingValueType.Value => _value, BindingValueType.BindingError => new BindingNotification(Error, BindingErrorType.Error), BindingValueType.BindingErrorWithFallback => @@ -190,7 +190,7 @@ public BindingValue WithValue(T value) /// /// The default value. /// The value. - public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue; + public T ValueOrDefault(T defaultValue = default) => HasValue ? _value : defaultValue; /// /// Gets the value of the binding value if present, otherwise a default value. @@ -204,7 +204,7 @@ public BindingValue WithValue(T value) public TResult ValueOrDefault(TResult defaultValue = default) { return HasValue ? - Value is TResult result ? result : default + _value is TResult result ? result : default : defaultValue; } diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index 3fbcd356ff1..5c9cbcec3cb 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -54,23 +54,23 @@ public Optional(T value) public bool Equals(Optional other) => this == other; /// - public override int GetHashCode() => HasValue ? Value!.GetHashCode() : 0; + 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 Optional ToObject() => HasValue ? new Optional(_value) : default; /// - public override string ToString() => HasValue ? Value?.ToString() ?? "(null)" : "(empty)"; + public override string ToString() => HasValue ? _value?.ToString() ?? "(null)" : "(empty)"; /// /// Gets the value if present, otherwise a default value. /// /// The default value. /// The value. - public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue; + public T ValueOrDefault(T defaultValue = default) => HasValue ? _value : defaultValue; /// /// Gets the value if present, otherwise a default value. @@ -84,7 +84,7 @@ public Optional(T value) public TResult ValueOrDefault(TResult defaultValue = default) { return HasValue ? - Value is TResult result ? result : default + _value is TResult result ? result : default : defaultValue; } From 8196e054b6f0482f634fec17bad0e35b1011d6e2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Dec 2019 11:27:00 +0100 Subject: [PATCH 24/50] ValueOrDefault -> GetValueOrDefault. To match `Nullable`. --- src/Avalonia.Animation/Animatable.cs | 4 ++-- .../AvaloniaPropertyChangedEventArgs`1.cs | 4 ++-- src/Avalonia.Base/Data/BindingValue.cs | 4 ++-- src/Avalonia.Base/Data/Optional.cs | 4 ++-- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 10 +++++----- src/Avalonia.Layout/StackLayout.cs | 2 +- src/Avalonia.Layout/UniformGridLayout.cs | 14 +++++++------- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index ac2fd5b9845..cc1ac8ded65 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -85,8 +85,8 @@ protected override void OnPropertyChanged( var instance = transition.Apply( this, Clock ?? Avalonia.Animation.Clock.GlobalClock, - oldValue.ValueOrDefault(), - newValue.ValueOrDefault()); + oldValue.GetValueOrDefault(), + newValue.GetValueOrDefault()); _previousTransitions[property] = instance; return; diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs index 0c7cd87897e..d8ac3752b34 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -60,8 +60,8 @@ public AvaloniaPropertyChangedEventArgs( protected override AvaloniaProperty GetProperty() => Property; - protected override object? GetOldValue() => OldValue.ValueOrDefault(AvaloniaProperty.UnsetValue); + protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); - protected override object? GetNewValue() => NewValue.ValueOrDefault(AvaloniaProperty.UnsetValue); + protected override object? GetNewValue() => NewValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); } } diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index e1f53698993..33a893efcab 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -190,7 +190,7 @@ public BindingValue WithValue(T value) /// /// The default value. /// The value. - public T ValueOrDefault(T defaultValue = default) => HasValue ? _value : defaultValue; + public T GetValueOrDefault(T defaultValue = default) => HasValue ? _value : defaultValue; /// /// Gets the value of the binding value if present, otherwise a default value. @@ -201,7 +201,7 @@ public BindingValue WithValue(T value) /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult ValueOrDefault(TResult defaultValue = default) + public TResult GetValueOrDefault(TResult defaultValue = default) { return HasValue ? _value is TResult result ? result : default diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index 5c9cbcec3cb..9ad15871948 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -70,7 +70,7 @@ public Optional(T value) /// /// The default value. /// The value. - public T ValueOrDefault(T defaultValue = default) => HasValue ? _value : defaultValue; + public T GetValueOrDefault(T defaultValue = default) => HasValue ? _value : defaultValue; /// /// Gets the value if present, otherwise a default value. @@ -81,7 +81,7 @@ public Optional(T value) /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult ValueOrDefault(TResult defaultValue = default) + public TResult GetValueOrDefault(TResult defaultValue = default) { return HasValue ? _value is TResult result ? result : default diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 457d72bd600..8cf95100098 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -379,7 +379,7 @@ protected override void OnPropertyChanged(AvaloniaProperty property, Optio { if (property == ItemsProperty) { - var newEnumerable = newValue.ValueOrDefault(); + var newEnumerable = newValue.GetValueOrDefault(); var newDataSource = newEnumerable as ItemsSourceView; if (newEnumerable != null && newDataSource == null) { @@ -390,19 +390,19 @@ protected override void OnPropertyChanged(AvaloniaProperty property, Optio } else if (property == ItemTemplateProperty) { - OnItemTemplateChanged(oldValue.ValueOrDefault(), newValue.ValueOrDefault()); + OnItemTemplateChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); } else if (property == LayoutProperty) { - OnLayoutChanged(oldValue.ValueOrDefault(), newValue.ValueOrDefault()); + OnLayoutChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); } else if (property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = newValue.ValueOrDefault(); + _viewportManager.HorizontalCacheLength = newValue.GetValueOrDefault(); } else if (property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = newValue.ValueOrDefault(); + _viewportManager.VerticalCacheLength = newValue.GetValueOrDefault(); } base.OnPropertyChanged(property, oldValue, newValue, priority); diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e95a87dd16d..8a372349ae8 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -298,7 +298,7 @@ protected override void OnPropertyChanged(AvaloniaProperty property, Optio { if (property == OrientationProperty) { - var orientation = newValue.ValueOrDefault(); + 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 feaf98553da..6d05c5badad 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -441,7 +441,7 @@ protected override void OnPropertyChanged(AvaloniaProperty property, Optio { if (property == OrientationProperty) { - var orientation = newValue.ValueOrDefault(); + 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. @@ -450,29 +450,29 @@ protected override void OnPropertyChanged(AvaloniaProperty property, Optio } else if (property == MinColumnSpacingProperty) { - _minColumnSpacing = newValue.ValueOrDefault(); + _minColumnSpacing = newValue.GetValueOrDefault(); } else if (property == MinRowSpacingProperty) { - _minRowSpacing = newValue.ValueOrDefault(); + _minRowSpacing = newValue.GetValueOrDefault(); } else if (property == ItemsJustificationProperty) { - _itemsJustification = newValue.ValueOrDefault(); + _itemsJustification = newValue.GetValueOrDefault(); ; } else if (property == ItemsStretchProperty) { - _itemsStretch = newValue.ValueOrDefault(); + _itemsStretch = newValue.GetValueOrDefault(); ; } else if (property == MinItemWidthProperty) { - _minItemWidth = newValue.ValueOrDefault(); + _minItemWidth = newValue.GetValueOrDefault(); } else if (property == MinItemHeightProperty) { - _minItemHeight = newValue.ValueOrDefault(); + _minItemHeight = newValue.GetValueOrDefault(); } InvalidateLayout(); From 9137cc828ae1f54d71e928ebb1bda67d753e09bc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Dec 2019 11:30:29 +0100 Subject: [PATCH 25/50] Added overload of GetValueOrDefault that takes no param. To match `Nullable`. --- src/Avalonia.Base/Data/BindingValue.cs | 20 ++++++++++++++++++++ src/Avalonia.Base/Data/Optional.cs | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 33a893efcab..da3f63ae9cd 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -185,6 +185,12 @@ public BindingValue WithValue(T value) 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. /// @@ -192,6 +198,20 @@ public BindingValue WithValue(T value) /// The value. public T GetValueOrDefault(T defaultValue = default) => 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. /// diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index 9ad15871948..eae9cc0a2f1 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -65,6 +65,12 @@ public Optional(T value) /// 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. /// @@ -72,6 +78,20 @@ public Optional(T value) /// The value. public T GetValueOrDefault(T defaultValue = default) => 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. /// From 37db6c79e025ac9dfc38d976b8d1aa6b61957c31 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 17 Dec 2019 12:39:39 +0100 Subject: [PATCH 26/50] Fix one-way bindings with notifications. --- src/Avalonia.Base/Data/BindingOperations.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 944a6c7799f..1b47cc7490e 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -64,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 { From 623a925c397feecf207330bfb8bdcc0fe34bc83f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 17 Dec 2019 12:42:43 +0100 Subject: [PATCH 27/50] Clarify difference between BindingValue and BindingNotifcation. --- src/Avalonia.Base/Data/BindingNotification.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index a568c062d09..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 { /// From 42bbb5e518a586d33f0c3cc22d1abbe94739d36f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 18 Dec 2019 22:33:01 +0100 Subject: [PATCH 28/50] Removed unused methods. --- src/Avalonia.Base/AvaloniaObject.cs | 46 ----------------------------- 1 file changed, 46 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e6d54727b35..f52638d948d 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -400,52 +400,6 @@ public void SetValue(DirectPropertyBase property, T value) SetDirectValueUnchecked(property, value); } - /// - /// Binds a to an observable. - /// - /// 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) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); - - return property.RouteBind(this, source, priority); - } - - /// - /// Binds a to an observable. - /// - /// The type of the property. - /// The property. - /// The observable. - /// The priority of the binding. - /// - /// A disposable which can be used to terminate the binding. - /// - public IDisposable Bind( - AvaloniaProperty property, - IObservable> source, - BindingPriority priority = BindingPriority.LocalValue) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); - - return property switch - { - StyledPropertyBase styled => Bind(styled, source, priority), - DirectPropertyBase direct => Bind(direct, source), - _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), - }; - } - /// /// Binds a to an observable. /// From 3a914c282b9c83b82b64bc1031d6c62e29ed03cb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 18 Dec 2019 22:55:37 +0100 Subject: [PATCH 29/50] Don't declare property fields AvaloniaProperty<>. Instead use `StyledProperty<>`. --- .../Primitives/DataGridFrozenGrid.cs | 2 +- src/Avalonia.Controls/GridSplitter.cs | 12 ++++++------ src/Avalonia.Controls/LayoutTransformControl.cs | 4 ++-- src/Avalonia.Controls/Presenters/ContentPresenter.cs | 2 +- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 8 ++++---- src/Avalonia.Input/DragDrop.cs | 2 +- src/Avalonia.ReactiveUI/ReactiveUserControl.cs | 2 +- src/Avalonia.ReactiveUI/ReactiveWindow.cs | 4 ++-- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 4 ++-- .../TransitioningContentControl.cs | 6 +++--- src/Avalonia.Visuals/Media/DashStyle.cs | 4 ++-- src/Avalonia.Visuals/Media/PolylineGeometry.cs | 2 +- src/Avalonia.Visuals/Media/TransformGroup.cs | 2 +- .../Primitives/PopupRootTests.cs | 2 +- .../Media/GeometryTests.cs | 2 +- 15 files changed, 29 insertions(+), 29 deletions(-) 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/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/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/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 8cf95100098..8f5d75119a4 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -22,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); /// @@ -40,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); 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.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.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/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/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.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() From 47368017a7f64aa2a290776ecde08b6bab212dfa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 18 Dec 2019 22:56:05 +0100 Subject: [PATCH 30/50] Remove unused methods. --- src/Avalonia.Base/AvaloniaObject.cs | 45 ----------------------------- 1 file changed, 45 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index f52638d948d..160e6a3ca9f 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -230,24 +230,6 @@ public object GetValue(AvaloniaProperty property) return property.RouteGetValue(this); } - /// - /// Gets a value. - /// - /// The type of the property. - /// The property. - /// The value. - public T GetValue(AvaloniaProperty property) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - - return property switch - { - StyledPropertyBase styled => GetValue(styled), - DirectPropertyBase direct => GetValue(direct), - _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") - }; - } - /// /// Gets a value. /// @@ -323,33 +305,6 @@ public void SetValue( 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( - AvaloniaProperty property, - T value, - BindingPriority priority = BindingPriority.LocalValue) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - - switch (property) - { - case StyledPropertyBase styled: - SetValue(styled, value, priority); - break; - case DirectPropertyBase direct: - SetValue(direct, value); - break; - default: - throw new NotSupportedException("Unsupported AvaloniaProperty type."); - } - } - /// /// Sets a value. /// From 5805eaf0f9a772f5a18d9b0f487d97fee737ffa3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 14 Jan 2020 10:36:04 +0100 Subject: [PATCH 31/50] Fix compile errors after merge with master. Due to `OnPropertyChanged` signature changing on branch. --- src/Avalonia.Layout/UniformGridLayout.cs | 5 ++--- src/Avalonia.Visuals/Media/DrawingImage.cs | 11 ++++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index ee1d88940c0..54c3ccbb90b 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -490,7 +490,6 @@ protected override void OnPropertyChanged(AvaloniaProperty property, Optio else if (property == ItemsStretchProperty) { _itemsStretch = newValue.GetValueOrDefault(); - ; } else if (property == MinItemWidthProperty) { @@ -500,9 +499,9 @@ protected override void OnPropertyChanged(AvaloniaProperty property, Optio { _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.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); } From 685f3f7b6ef6c47aa4f953a94733b388bdf53159 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Jan 2020 22:31:15 +0100 Subject: [PATCH 32/50] Fix typos. --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- src/Avalonia.Base/Data/BindingValue.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 160e6a3ca9f..e0fa93c341b 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -331,7 +331,7 @@ public void SetValue( else { throw new NotSupportedException( - "Canot set property to Unset at non-local value priority."); + "Cannot set property to Unset at non-local value priority."); } } else if (!(value is DoNothingType)) diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index da3f63ae9cd..e220bb06736 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -161,7 +161,7 @@ private BindingValue(BindingValueType type, T value, Exception? error) new BindingNotification(Error, BindingErrorType.DataValidationError), BindingValueType.DataValidationErrorWithFallback => new BindingNotification(Error, BindingErrorType.DataValidationError, Value), - _ => throw new NotSupportedException("Invalida BindingValueType."), + _ => throw new NotSupportedException("Invalid BindingValueType."), }; } From afce4460dd280ca6ff0745d711d10c76c9833861 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Jan 2020 22:42:11 +0100 Subject: [PATCH 33/50] Removed useless default values. There's already an overload without the parameter. --- src/Avalonia.Base/Data/BindingValue.cs | 4 ++-- src/Avalonia.Base/Data/Optional.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index e220bb06736..a35351b8748 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -196,7 +196,7 @@ public BindingValue WithValue(T value) /// /// The default value. /// The value. - public T GetValueOrDefault(T defaultValue = default) => HasValue ? _value : defaultValue; + public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; /// /// Gets the value if present, otherwise the default value. @@ -221,7 +221,7 @@ public TResult GetValueOrDefault() /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult GetValueOrDefault(TResult defaultValue = default) + public TResult GetValueOrDefault(TResult defaultValue) { return HasValue ? _value is TResult result ? result : default diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index eae9cc0a2f1..dd952c895c7 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -76,7 +76,7 @@ public Optional(T value) /// /// The default value. /// The value. - public T GetValueOrDefault(T defaultValue = default) => HasValue ? _value : defaultValue; + public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; /// /// Gets the value if present, otherwise the default value. @@ -101,7 +101,7 @@ public TResult GetValueOrDefault() /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult GetValueOrDefault(TResult defaultValue = default) + public TResult GetValueOrDefault(TResult defaultValue) { return HasValue ? _value is TResult result ? result : default From 00c1c3512592a3c09b84958d855008c37c535a61 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Jan 2020 22:42:59 +0100 Subject: [PATCH 34/50] Go direct to property store. To avoid a call to `VerifyAccess` (which shows up when profiling). --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e0fa93c341b..023594de9ab 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -431,7 +431,7 @@ void IAvaloniaObject.InheritedPropertyChanged( Optional oldValue, Optional newValue) { - if (property.Inherits && !IsSet(property)) + if (property.Inherits && (_values == null || !_values.IsSet(property))) { RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); } From d0e2a844db6dd3b4c101b3ebda4249c429d19c0d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Jan 2020 22:50:45 +0100 Subject: [PATCH 35/50] Removed unused owner reference. --- src/Avalonia.Base/PropertyStore/BindingEntry.cs | 3 --- src/Avalonia.Base/PropertyStore/PriorityValue.cs | 2 +- src/Avalonia.Base/ValueStore.cs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 2a7cf098a56..cb51f1c9d02 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -12,18 +12,15 @@ internal interface IBindingEntry : IPriorityValueEntry, IDisposable 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; diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 1a4b616bd9a..640570237d3 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -77,7 +77,7 @@ public void SetValue(T value, BindingPriority priority) public BindingEntry AddBinding(IObservable> source, BindingPriority priority) { - var binding = new BindingEntry(_owner, Property, source, priority, this); + var binding = new BindingEntry(Property, source, priority, this); var insert = FindInsertPoint(binding.Priority); _entries.Insert(insert, binding); return binding; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 44ee1c4c8fc..23adfb4c934 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -94,7 +94,7 @@ public IDisposable AddBinding( } else { - var entry = new BindingEntry(_owner, property, source, priority, this); + var entry = new BindingEntry(property, source, priority, this); _values.AddValue(property, entry); entry.Start(); return entry; From fa81c42c5645f3b091feef3783285055953e6d01 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 10:51:37 +0100 Subject: [PATCH 36/50] BindingValue should be marked [Flags]. --- src/Avalonia.Base/Data/BindingValue.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index a35351b8748..cecdd33e7b4 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -8,6 +8,7 @@ namespace Avalonia.Data /// /// Describes the type of a . /// + [Flags] public enum BindingValueType { /// From f7f9e41bf73f069d949d7cff0ca89c7ec55aa8c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 10:51:44 +0100 Subject: [PATCH 37/50] Setter not needed. --- src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index bc75eac4ef9..0d1240f689c 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -19,7 +19,7 @@ public ConstantValueEntry( public StyledPropertyBase Property { get; } public BindingPriority Priority { get; } - public Optional Value { get; private set; } + public Optional Value { get; } Optional IValue.Value => Value.ToObject(); BindingPriority IValue.ValuePriority => Priority; From 270f9718b835b51c2d88ea2e2bf353c314fee2df Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 10:54:13 +0100 Subject: [PATCH 38/50] Removed unused field. --- .../PropertyStore/PriorityValue.cs | 9 ++------ src/Avalonia.Base/ValueStore.cs | 8 +++---- .../PriorityValueTests.cs | 21 ++++++++----------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 640570237d3..5f117b26cf1 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -8,27 +8,23 @@ namespace Avalonia.PropertyStore { internal class PriorityValue : IValue, IValueSink { - private readonly IAvaloniaObject _owner; private readonly IValueSink _sink; private readonly List> _entries = new List>(); private Optional _localValue; public PriorityValue( - IAvaloniaObject owner, StyledPropertyBase property, IValueSink sink) { - _owner = owner; Property = property; _sink = sink; } public PriorityValue( - IAvaloniaObject owner, StyledPropertyBase property, IValueSink sink, IPriorityValueEntry existing) - : this(owner, property, sink) + : this(property, sink) { existing.Reparent(this); _entries.Add(existing); @@ -41,11 +37,10 @@ public PriorityValue( } public PriorityValue( - IAvaloniaObject owner, StyledPropertyBase property, IValueSink sink, LocalValueEntry existing) - : this(owner, property, sink) + : this(property, sink) { _localValue = existing.Value; Value = _localValue; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 23adfb4c934..c0e704b6ccf 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -174,7 +174,7 @@ private void SetExisting( { if (slot is IPriorityValueEntry e) { - var priorityValue = new PriorityValue(_owner, property, this, e); + var priorityValue = new PriorityValue(property, this, e); _values.SetValue(property, priorityValue); priorityValue.SetValue(value, priority); } @@ -192,7 +192,7 @@ private void SetExisting( } else { - var priorityValue = new PriorityValue(_owner, property, this, l); + var priorityValue = new PriorityValue(property, this, l); _values.SetValue(property, priorityValue); } } @@ -212,7 +212,7 @@ private IDisposable BindExisting( if (slot is IPriorityValueEntry e) { - priorityValue = new PriorityValue(_owner, property, this, e); + priorityValue = new PriorityValue(property, this, e); } else if (slot is PriorityValue p) { @@ -220,7 +220,7 @@ private IDisposable BindExisting( } else if (slot is LocalValueEntry l) { - priorityValue = new PriorityValue(_owner, property, this, l); + priorityValue = new PriorityValue(property, this, l); } else { diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 8c76445645d..9f69c42e52c 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -21,7 +21,6 @@ public class PriorityValueTests public void Constructor_Should_Set_Value_Based_On_Initial_Entry() { var target = new PriorityValue( - Owner, TestProperty, NullSink, new ConstantValueEntry(TestProperty, "1", BindingPriority.StyleTrigger)); @@ -34,7 +33,6 @@ public void Constructor_Should_Set_Value_Based_On_Initial_Entry() public void SetValue_LocalValue_Should_Not_Add_Entries() { var target = new PriorityValue( - Owner, TestProperty, NullSink); @@ -48,7 +46,6 @@ public void SetValue_LocalValue_Should_Not_Add_Entries() public void SetValue_Non_LocalValue_Should_Add_Entries() { var target = new PriorityValue( - Owner, TestProperty, NullSink); @@ -66,7 +63,7 @@ public void SetValue_Non_LocalValue_Should_Add_Entries() [Fact] public void Binding_With_Same_Priority_Should_Be_Appended() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); @@ -86,7 +83,7 @@ public void Binding_With_Same_Priority_Should_Be_Appended() [Fact] public void Binding_With_Higher_Priority_Should_Be_Appended() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); @@ -106,7 +103,7 @@ public void Binding_With_Higher_Priority_Should_Be_Appended() [Fact] public void Binding_With_Lower_Priority_Should_Be_Prepended() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); @@ -126,7 +123,7 @@ public void Binding_With_Lower_Priority_Should_Be_Prepended() [Fact] public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); var source3 = new Source("3"); @@ -148,7 +145,7 @@ public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() [Fact] public void Competed_Binding_Should_Be_Removed() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); var source3 = new Source("3"); @@ -171,7 +168,7 @@ public void Competed_Binding_Should_Be_Removed() [Fact] public void Value_Should_Come_From_Last_Entry() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); var source3 = new Source("3"); @@ -186,7 +183,7 @@ public void Value_Should_Come_From_Last_Entry() [Fact] public void LocalValue_Should_Override_LocalValue_Binding() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); target.AddBinding(source1, BindingPriority.LocalValue).Start(); @@ -198,7 +195,7 @@ public void LocalValue_Should_Override_LocalValue_Binding() [Fact] public void LocalValue_Should_Override_Style_Binding() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); target.AddBinding(source1, BindingPriority.Style).Start(); @@ -210,7 +207,7 @@ public void LocalValue_Should_Override_Style_Binding() [Fact] public void LocalValue_Should_Not_Override_Animation_Binding() { - var target = new PriorityValue(Owner, TestProperty, NullSink); + var target = new PriorityValue(TestProperty, NullSink); var source1 = new Source("1"); target.AddBinding(source1, BindingPriority.Animation).Start(); From 1ff6e35a06ee10d7486d2d3a6cbbcecf7840cb10 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 10:56:30 +0100 Subject: [PATCH 39/50] Disable nullable around generic T. Compiler warns that `_value` is potentially null, but it can be a value or refernce type. --- src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs index 95016406268..be044b05591 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -12,6 +12,7 @@ internal class AvaloniaPropertyBindingObservable : LightweightObservableBase< private readonly AvaloniaProperty _property; private T _value; +#nullable disable public AvaloniaPropertyBindingObservable( IAvaloniaObject target, AvaloniaProperty property) @@ -19,6 +20,7 @@ public AvaloniaPropertyBindingObservable( _target = new WeakReference(target); _property = property; } +#nullable enable public string Description => $"{_target.GetType().Name}.{_property.Name}"; From c2d5f623900051910d105d661715479b9ca6d5e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 11:01:35 +0100 Subject: [PATCH 40/50] Avoid boxing if property is correct type. For typed bindings, we'll have an `AvaloniaPropertyChangedEventArgs` where `T` is the same as the type of the `AvaloniaPropertyObservable`. For non-typed bindings we'll have `object` as `T` so use the non-typed `AvaloniaProperty`. --- .../Reactive/AvaloniaPropertyObservable.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index 100330ed1d3..238aba5c962 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -44,7 +44,16 @@ private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _property) { - var newValue = e.Sender.GetValue(e.Property); + 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)) { From 3a80cee3fc869cddf66625d89459fb83d966bbad Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 11:02:28 +0100 Subject: [PATCH 41/50] Add nullability annotations. --- src/Avalonia.Base/Reactive/BindingValueExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs index 6871602ad9f..6f0d29dd0ff 100644 --- a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs +++ b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs @@ -20,13 +20,13 @@ public static ISubject> ToBindingValue(this ISubject sourc return new BindingValueSubjectAdapter(source); } - public static IObservable ToUntyped(this IObservable> 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) + public static ISubject ToUntyped(this ISubject> source) { source = source ?? throw new ArgumentNullException(nameof(source)); return new UntypedBindingSubjectAdapter(source); From 6f92e3786aac21964023705c90b05065000dbb38 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 11:02:54 +0100 Subject: [PATCH 42/50] Fix typo. --- src/Avalonia.Base/IAvaloniaObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 4fec3d71afe..c8150e8a640 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -111,7 +111,7 @@ IDisposable Bind( /// /// The inheritance child. /// - /// Inheritance children will recieve a call to + /// Inheritance children will receive a call to /// /// when an inheritable property value changes on the parent. /// From a1353c7715062e4d3ad4268c26a31f12e44584e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 11:03:13 +0100 Subject: [PATCH 43/50] Remove unused method. --- src/Avalonia.Base/StyledPropertyMetadata`1.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index d4ce137e0a4..18a38655bd0 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -45,18 +45,5 @@ public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty prope } } } - - [DebuggerHidden] - private static Func Cast(Func f) - { - if (f == null) - { - return null; - } - else - { - return (o, v) => f(o, (TValue)v); - } - } } } From 4cc378ea0f96240930f950bd90ffa2ebec486e2e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 11:13:37 +0100 Subject: [PATCH 44/50] Property store can be typed on IValue. --- src/Avalonia.Base/ValueStore.cs | 38 ++++++++++++--------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index c0e704b6ccf..2e2657086d3 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -12,22 +12,19 @@ internal class ValueStore : IValueSink { private readonly AvaloniaObject _owner; private readonly IValueSink _sink; - private readonly AvaloniaPropertyValueStore _values; + private readonly AvaloniaPropertyValueStore _values; public ValueStore(AvaloniaObject owner) { _sink = _owner = owner; - _values = new AvaloniaPropertyValueStore(); + _values = new AvaloniaPropertyValueStore(); } public bool IsAnimating(AvaloniaProperty property) { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue v) - { - return v.ValuePriority < BindingPriority.LocalValue; - } + return slot.ValuePriority < BindingPriority.LocalValue; } return false; @@ -37,10 +34,7 @@ public bool IsSet(AvaloniaProperty property) { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue v) - { - return v.Value.HasValue; - } + return slot.Value.HasValue; } return false; @@ -50,13 +44,12 @@ public bool TryGetValue(StyledPropertyBase property, out T value) { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue v) + var v = (IValue)slot; + + if (v.Value.HasValue) { - if (v.Value.HasValue) - { - value = v.Value.Value; - return true; - } + value = v.Value.Value; + return true; } } @@ -133,14 +126,11 @@ public void ClearLocalValue(StyledPropertyBase property) { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue value) - { - return new Diagnostics.AvaloniaPropertyValue( - property, - value.Value.HasValue ? (object)value.Value : AvaloniaProperty.UnsetValue, - value.ValuePriority, - null); - } + return new Diagnostics.AvaloniaPropertyValue( + property, + slot.Value.HasValue ? (object)slot.Value : AvaloniaProperty.UnsetValue, + slot.ValuePriority, + null); } return null; From 69e702320bd05519a9d9d6d35547a4dceaefb55a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 20 Jan 2020 11:30:49 +0100 Subject: [PATCH 45/50] Added some documentation to value stores. --- src/Avalonia.Base/PropertyStore/BindingEntry.cs | 7 +++++++ .../PropertyStore/ConstantValueEntry.cs | 5 +++++ .../PropertyStore/IPriorityValueEntry.cs | 7 +++++++ src/Avalonia.Base/PropertyStore/IValue.cs | 7 +++++++ src/Avalonia.Base/PropertyStore/IValueSink.cs | 6 ++++-- .../PropertyStore/LocalValueEntry.cs | 5 +++++ src/Avalonia.Base/PropertyStore/PriorityValue.cs | 11 +++++++++++ src/Avalonia.Base/ValueStore.cs | 15 ++++++++++++++- 8 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index cb51f1c9d02..79e55e7e020 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -6,10 +6,17 @@ 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 IValueSink _sink; diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index 0d1240f689c..f15f56e32b7 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -5,6 +5,11 @@ namespace Avalonia.PropertyStore { + /// + /// Stores a value with a priority in a or + /// . + /// + /// The property type. internal class ConstantValueEntry : IPriorityValueEntry { public ConstantValueEntry( diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs index 8e239e03c93..6ed6c2ef52f 100644 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -5,6 +5,9 @@ namespace Avalonia.PropertyStore { + /// + /// Represents an untyped interface to . + /// internal interface IPriorityValueEntry : IValue { BindingPriority Priority { get; } @@ -12,6 +15,10 @@ internal interface IPriorityValueEntry : IValue 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 index 7d1eaa337fc..0ce7fb83088 100644 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -4,12 +4,19 @@ 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 index faccd9e75ae..223b0058c1d 100644 --- a/src/Avalonia.Base/PropertyStore/IValueSink.cs +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -1,10 +1,12 @@ -using System; -using Avalonia.Data; +using Avalonia.Data; #nullable enable namespace Avalonia.PropertyStore { + /// + /// Represents an entity that can receive change notifications in a . + /// internal interface IValueSink { void ValueChanged( diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 067ed7b9663..22258390dab 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -4,6 +4,11 @@ namespace Avalonia.PropertyStore { + /// + /// Stores a value with local value priority in a or + /// . + /// + /// The property type. internal class LocalValueEntry : IValue { private T _value; diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 5f117b26cf1..a7b17d6f5a9 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -6,6 +6,17 @@ 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 IValueSink _sink; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 2e2657086d3..5b6285623e6 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia.Data; using Avalonia.PropertyStore; using Avalonia.Utilities; @@ -8,6 +7,20 @@ namespace Avalonia { + /// + /// Stores styled property values for an . + /// + /// + /// At its core this class consists of an to + /// mapping which holds the current values for each set property. This + /// can be in one of 4 states: + /// + /// - For a single local value it will be an instance of . + /// - For a single value of a priority other than LocalValue it will be an instance of + /// ` + /// - For a single binding it will be an instance of + /// - For all other cases it will be an instance of + /// internal class ValueStore : IValueSink { private readonly AvaloniaObject _owner; From c6acca57c419d87decd3f5001c55b832bd77098e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jan 2020 19:07:48 +0100 Subject: [PATCH 46/50] Fix method name after merge. --- src/Avalonia.Base/StyledPropertyMetadata`1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index bd6c1257767..ebe8a485d5c 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -33,7 +33,7 @@ public StyledPropertyMetadata( /// /// Gets the default value for the property. /// - public TValue DefaultValue => _defaultValue.ValueOrDefault(); + public TValue DefaultValue => _defaultValue.GetValueOrDefault(); /// /// Gets the value coercion callback, if any. From 7f26635efaa028c4841f1a425c6b9fb2150a9905 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jan 2020 19:27:26 +0100 Subject: [PATCH 47/50] Wire up validation/coercion for attached properties. --- src/Avalonia.Base/AttachedProperty.cs | 8 +++++--- src/Avalonia.Base/AvaloniaProperty.cs | 20 +++++++++++++------ src/Avalonia.Controls/DefinitionBase.cs | 6 +++++- .../AvaloniaObjectTests_Coercion.cs | 16 +++++++++++++++ .../AvaloniaObjectTests_Validation.cs | 15 ++++++++++++++ 5 files changed, 55 insertions(+), 10 deletions(-) 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/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 8e5716a5bf6..e1d4a23441f 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -304,22 +304,25 @@ public static StyledProperty Register( /// Whether the property inherits its value. /// The default binding mode for the property. /// 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, - 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); @@ -336,22 +339,27 @@ public static AttachedProperty RegisterAttached( /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A value validation callback. + /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, Type ownerType, TValue defaultValue = default(TValue), bool inherits = false, - BindingMode defaultBindingMode = BindingMode.OneWay) + BindingMode defaultBindingMode = BindingMode.OneWay, + Func validate = null, + Func coerce = null) where THost : IAvaloniaObject { Contract.Requires(name != null); var metadata = new StyledPropertyMetadata( defaultValue, - 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); diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 6f121afbef1..38ebbe5bf92 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -358,7 +358,11 @@ private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaP /// private static bool SharedSizeGroupPropertyValueValid(string value) { - Contract.Requires(value != null); + // null is default value + if (value == null) + { + return true; + } string id = (string)value; diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index 8d8dbb03a22..11f5a66400f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -20,6 +20,16 @@ public void Coerces_Set_Value() 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() { @@ -98,6 +108,12 @@ private class Class1 : AvaloniaObject defaultValue: 11, coerce: CoerceFoo); + public static readonly AttachedProperty AttachedProperty = + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + coerce: CoerceFoo); + public int Foo { get => GetValue(FooProperty); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index 70de7b449f5..9e48c79106c 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 @@ -34,6 +35,14 @@ public void SetValue_Throws_If_Fails_Validation() Assert.Throws(() => target.SetValue(Class1.FooProperty, 101)); } + [Fact] + public void SetValue_Throws_If_Fails_Validation_Attached() + { + var target = new Class1(); + + Assert.Throws(() => target.SetValue(Class1.AttachedProperty, 101)); + } + [Fact] public void Reverts_To_DefaultValue_If_Binding_Fails_Validation() { @@ -69,6 +78,12 @@ private class Class1 : AvaloniaObject defaultValue: 11, validate: ValidateFoo); + public static readonly AttachedProperty AttachedProperty = + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + validate: ValidateFoo); + public static bool ValidateFoo(int value) { return value < 100; From 89ba4a63278dc4dbdae7f4b182f071e6aaedd095 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jan 2020 19:44:10 +0100 Subject: [PATCH 48/50] Prevent tests interfering with other tests. --- tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs | 2 +- tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index 11f5a66400f..3efb926ac33 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -109,7 +109,7 @@ private class Class1 : AvaloniaObject coerce: CoerceFoo); public static readonly AttachedProperty AttachedProperty = - AvaloniaProperty.RegisterAttached( + AvaloniaProperty.RegisterAttached( "Attached", defaultValue: 11, coerce: CoerceFoo); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index 9e48c79106c..391b379c51c 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -79,7 +79,7 @@ private class Class1 : AvaloniaObject validate: ValidateFoo); public static readonly AttachedProperty AttachedProperty = - AvaloniaProperty.RegisterAttached( + AvaloniaProperty.RegisterAttached( "Attached", defaultValue: 11, validate: ValidateFoo); From 3bc1594dca46c3bb32a650063174b8324cfb3fb8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Jan 2020 10:05:26 +0100 Subject: [PATCH 49/50] Revert "Removed unused field." This reverts commit 270f9718b835b51c2d88ea2e2bf353c314fee2df. --- .../PropertyStore/PriorityValue.cs | 9 ++++++-- src/Avalonia.Base/ValueStore.cs | 8 +++---- .../PriorityValueTests.cs | 21 +++++++++++-------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 7440ba8e03e..d6139b29273 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -19,15 +19,18 @@ namespace Avalonia.PropertyStore /// 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; @@ -39,10 +42,11 @@ public PriorityValue( } public PriorityValue( + IAvaloniaObject owner, StyledPropertyBase property, IValueSink sink, IPriorityValueEntry existing) - : this(property, sink) + : this(owner, property, sink) { existing.Reparent(this); _entries.Add(existing); @@ -55,10 +59,11 @@ public PriorityValue( } public PriorityValue( + IAvaloniaObject owner, StyledPropertyBase property, IValueSink sink, LocalValueEntry existing) - : this(property, sink) + : this(owner, property, sink) { _localValue = existing.Value; Value = _localValue; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index a78f6d15d6a..af093c2b223 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -209,7 +209,7 @@ private void SetExisting( { if (slot is IPriorityValueEntry e) { - var priorityValue = new PriorityValue(property, this, e); + var priorityValue = new PriorityValue(_owner, property, this, e); _values.SetValue(property, priorityValue); priorityValue.SetValue(value, priority); } @@ -227,7 +227,7 @@ private void SetExisting( } else { - var priorityValue = new PriorityValue(property, this, l); + var priorityValue = new PriorityValue(_owner, property, this, l); _values.SetValue(property, priorityValue); } } @@ -247,7 +247,7 @@ private IDisposable BindExisting( if (slot is IPriorityValueEntry e) { - priorityValue = new PriorityValue(property, this, e); + priorityValue = new PriorityValue(_owner, property, this, e); } else if (slot is PriorityValue p) { @@ -255,7 +255,7 @@ private IDisposable BindExisting( } else if (slot is LocalValueEntry l) { - priorityValue = new PriorityValue(property, this, l); + priorityValue = new PriorityValue(_owner, property, this, l); } else { diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 9f69c42e52c..8c76445645d 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -21,6 +21,7 @@ public class PriorityValueTests public void Constructor_Should_Set_Value_Based_On_Initial_Entry() { var target = new PriorityValue( + Owner, TestProperty, NullSink, new ConstantValueEntry(TestProperty, "1", BindingPriority.StyleTrigger)); @@ -33,6 +34,7 @@ public void Constructor_Should_Set_Value_Based_On_Initial_Entry() public void SetValue_LocalValue_Should_Not_Add_Entries() { var target = new PriorityValue( + Owner, TestProperty, NullSink); @@ -46,6 +48,7 @@ public void SetValue_LocalValue_Should_Not_Add_Entries() public void SetValue_Non_LocalValue_Should_Add_Entries() { var target = new PriorityValue( + Owner, TestProperty, NullSink); @@ -63,7 +66,7 @@ public void SetValue_Non_LocalValue_Should_Add_Entries() [Fact] public void Binding_With_Same_Priority_Should_Be_Appended() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); @@ -83,7 +86,7 @@ public void Binding_With_Same_Priority_Should_Be_Appended() [Fact] public void Binding_With_Higher_Priority_Should_Be_Appended() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); @@ -103,7 +106,7 @@ public void Binding_With_Higher_Priority_Should_Be_Appended() [Fact] public void Binding_With_Lower_Priority_Should_Be_Prepended() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); @@ -123,7 +126,7 @@ public void Binding_With_Lower_Priority_Should_Be_Prepended() [Fact] public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); var source3 = new Source("3"); @@ -145,7 +148,7 @@ public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() [Fact] public void Competed_Binding_Should_Be_Removed() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); var source3 = new Source("3"); @@ -168,7 +171,7 @@ public void Competed_Binding_Should_Be_Removed() [Fact] public void Value_Should_Come_From_Last_Entry() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); var source2 = new Source("2"); var source3 = new Source("3"); @@ -183,7 +186,7 @@ public void Value_Should_Come_From_Last_Entry() [Fact] public void LocalValue_Should_Override_LocalValue_Binding() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); target.AddBinding(source1, BindingPriority.LocalValue).Start(); @@ -195,7 +198,7 @@ public void LocalValue_Should_Override_LocalValue_Binding() [Fact] public void LocalValue_Should_Override_Style_Binding() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); target.AddBinding(source1, BindingPriority.Style).Start(); @@ -207,7 +210,7 @@ public void LocalValue_Should_Override_Style_Binding() [Fact] public void LocalValue_Should_Not_Override_Animation_Binding() { - var target = new PriorityValue(TestProperty, NullSink); + var target = new PriorityValue(Owner, TestProperty, NullSink); var source1 = new Source("1"); target.AddBinding(source1, BindingPriority.Animation).Start(); From 399417692feb18dc163d5924dc535de9a33f7c4e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Jan 2020 10:11:14 +0100 Subject: [PATCH 50/50] Revert "Removed unused owner reference." This reverts commit d0e2a844db6dd3b4c101b3ebda4249c429d19c0d. --- src/Avalonia.Base/PropertyStore/BindingEntry.cs | 3 +++ src/Avalonia.Base/PropertyStore/PriorityValue.cs | 2 +- src/Avalonia.Base/ValueStore.cs | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 637272ad6a8..09a0f169df2 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -19,15 +19,18 @@ internal interface IBindingEntry : IPriorityValueEntry, IDisposable /// 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; diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index d6139b29273..2785dc6840c 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -95,7 +95,7 @@ public void SetValue(T value, BindingPriority priority) public BindingEntry AddBinding(IObservable> source, BindingPriority priority) { - var binding = new BindingEntry(Property, source, priority, this); + var binding = new BindingEntry(_owner, Property, source, priority, this); var insert = FindInsertPoint(binding.Priority); _entries.Insert(insert, binding); return binding; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index af093c2b223..58ebc486522 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -121,7 +121,7 @@ public IDisposable AddBinding( } else { - var entry = new BindingEntry(property, source, priority, this); + var entry = new BindingEntry(_owner, property, source, priority, this); _values.AddValue(property, entry); entry.Start(); return entry;