From 98fa10416f33509c8ef4a2007d67ad336eb4b1a8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Feb 2023 11:38:49 +0100 Subject: [PATCH] Use same logic for typed and untyped observers. However we still need to split the observers into two classes because otherwise we can't implement both `IObservable` and `IObservable` on the same object. --- .../LocalValueBindingObserver.cs | 119 ++---------------- ...er.cs => LocalValueBindingObserverBase.cs} | 57 +++++++-- src/Avalonia.Base/PropertyStore/ValueStore.cs | 2 +- 3 files changed, 55 insertions(+), 123 deletions(-) rename src/Avalonia.Base/PropertyStore/{LocalValueUntypedBindingObserver.cs => LocalValueBindingObserverBase.cs} (60%) diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 24eb00b2fe..9e9b4a3190 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -1,126 +1,25 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Data; -using Avalonia.Threading; namespace Avalonia.PropertyStore { - internal class LocalValueBindingObserver : IObserver, - IObserver>, - IDisposable + internal class LocalValueBindingObserver : LocalValueBindingObserverBase, + IObserver { - private readonly ValueStore _owner; - private readonly bool _hasDataValidation; - private IDisposable? _subscription; - private T? _defaultValue; - private bool _isDefaultValueInitialized; - public LocalValueBindingObserver(ValueStore owner, StyledProperty property) + : base(owner, property) { - _owner = owner; - Property = property; - _hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false; - } - - public StyledProperty Property { get;} - - public void Start(IObservable source) - { - _subscription = source.Subscribe(this); - } - - public void Start(IObservable> source) - { - _subscription = source.Subscribe(this); - } - - public void Dispose() - { - _subscription?.Dispose(); - _subscription = null; - _owner.OnLocalValueBindingCompleted(Property, this); } - public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); - public void OnError(Exception error) => OnCompleted(); + public void Start(IObservable source) => _subscription = source.Subscribe(this); - public void OnNext(T value) + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] + public void OnNext(object? value) { - static void Execute(LocalValueBindingObserver instance, T value) - { - var owner = instance._owner; - var property = instance.Property; - - if (property.ValidateValue?.Invoke(value) == false) - value = instance.GetCachedDefaultValue(); - - owner.SetLocalValue(property, value); - - if (instance._hasDataValidation) - owner.Owner.OnUpdateDataValidation(property, BindingValueType.Value, null); - } - - if (Dispatcher.UIThread.CheckAccess()) - { - Execute(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(() => Execute(instance, newValue)); - } - } - - public void OnNext(BindingValue value) - { - static void Execute(LocalValueBindingObserver instance, BindingValue value) - { - var owner = instance._owner; - var property = instance.Property; - var originalType = value.Type; - - LoggingUtils.LogIfNecessary(owner.Owner, property, value); - - // Revert to the default value if the binding value fails validation, or if - // there was no value (though not if there was a data validation error). - if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) || - (!value.HasValue && value.Type != BindingValueType.DataValidationError)) - value = value.WithValue(instance.GetCachedDefaultValue()); - - if (value.HasValue) - owner.SetLocalValue(property, value.Value); - if (instance._hasDataValidation) - owner.Owner.OnUpdateDataValidation(property, originalType, value.Error); - } - - if (value.Type is BindingValueType.DoNothing) + if (value == BindingOperations.DoNothing) return; - - if (Dispatcher.UIThread.CheckAccess()) - { - Execute(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(() => Execute(instance, newValue)); - } - } - - private T GetCachedDefaultValue() - { - if (!_isDefaultValueInitialized) - { - _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); - _isDefaultValueInitialized = true; - } - - return _defaultValue!; + base.OnNext(BindingValue.FromUntyped(value, Property.PropertyType)); } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs similarity index 60% rename from src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs rename to src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs index cda11faa1a..85de33d9e0 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs @@ -1,29 +1,34 @@ using System; -using System.Diagnostics.CodeAnalysis; using Avalonia.Data; using Avalonia.Threading; namespace Avalonia.PropertyStore { - internal class LocalValueUntypedBindingObserver : IObserver, + internal class LocalValueBindingObserverBase : IObserver, + IObserver>, IDisposable { private readonly ValueStore _owner; private readonly bool _hasDataValidation; - private IDisposable? _subscription; + protected IDisposable? _subscription; private T? _defaultValue; private bool _isDefaultValueInitialized; - public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty property) + protected LocalValueBindingObserverBase(ValueStore owner, StyledProperty property) { _owner = owner; Property = property; _hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false; } - public StyledProperty Property { get; } + public StyledProperty Property { get;} - public void Start(IObservable source) + public void Start(IObservable source) + { + _subscription = source.Subscribe(this); + } + + public void Start(IObservable> source) { _subscription = source.Subscribe(this); } @@ -38,14 +43,42 @@ namespace Avalonia.PropertyStore public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); public void OnError(Exception error) => OnCompleted(); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] - public void OnNext(object? value) + public void OnNext(T value) + { + static void Execute(LocalValueBindingObserverBase instance, T value) + { + var owner = instance._owner; + var property = instance.Property; + + if (property.ValidateValue?.Invoke(value) == false) + value = instance.GetCachedDefaultValue(); + + owner.SetLocalValue(property, value); + + if (instance._hasDataValidation) + owner.Owner.OnUpdateDataValidation(property, BindingValueType.Value, null); + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(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(() => Execute(instance, newValue)); + } + } + + public void OnNext(BindingValue value) { - static void Execute(LocalValueUntypedBindingObserver instance, object? untypedValue) + static void Execute(LocalValueBindingObserverBase instance, BindingValue value) { var owner = instance._owner; var property = instance.Property; - var value = BindingValue.FromUntyped(untypedValue, property.PropertyType); var originalType = value.Type; LoggingUtils.LogIfNecessary(owner.Owner, property, value); @@ -62,14 +95,14 @@ namespace Avalonia.PropertyStore owner.Owner.OnUpdateDataValidation(property, originalType, value.Error); } - if (value == BindingOperations.DoNothing) + if (value.Type is BindingValueType.DoNothing) return; if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); } - else if (value != BindingOperations.DoNothing) + 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. diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 9efc91d44d..0a5084466f 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -104,7 +104,7 @@ namespace Avalonia.PropertyStore { if (priority == BindingPriority.LocalValue) { - var observer = new LocalValueUntypedBindingObserver(this, property); + var observer = new LocalValueBindingObserver(this, property); DisposeExistingLocalValueBinding(property); _localValueBindings ??= new(); _localValueBindings[property.Id] = observer;