Browse Source

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<T>` and `IObservable<object?>` on the same object.
pull/10423/head
Steven Kirk 3 years ago
parent
commit
98fa10416f
  1. 119
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  2. 57
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs
  3. 2
      src/Avalonia.Base/PropertyStore/ValueStore.cs

119
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<T> : IObserver<T>,
IObserver<BindingValue<T>>,
IDisposable
internal class LocalValueBindingObserver<T> : LocalValueBindingObserverBase<T>,
IObserver<object?>
{
private readonly ValueStore _owner;
private readonly bool _hasDataValidation;
private IDisposable? _subscription;
private T? _defaultValue;
private bool _isDefaultValueInitialized;
public LocalValueBindingObserver(ValueStore owner, StyledProperty<T> property)
: base(owner, property)
{
_owner = owner;
Property = property;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false;
}
public StyledProperty<T> Property { get;}
public void Start(IObservable<T> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<BindingValue<T>> 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<object?> 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<T> 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<T> value)
{
static void Execute(LocalValueBindingObserver<T> instance, BindingValue<T> 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<T>.FromUntyped(value, Property.PropertyType));
}
}
}

57
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs → 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<T> : IObserver<object?>,
internal class LocalValueBindingObserverBase<T> : IObserver<T>,
IObserver<BindingValue<T>>,
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<T> property)
protected LocalValueBindingObserverBase(ValueStore owner, StyledProperty<T> property)
{
_owner = owner;
Property = property;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false;
}
public StyledProperty<T> Property { get; }
public StyledProperty<T> Property { get;}
public void Start(IObservable<object?> source)
public void Start(IObservable<T> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<BindingValue<T>> 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<T> 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<T> value)
{
static void Execute(LocalValueUntypedBindingObserver<T> instance, object? untypedValue)
static void Execute(LocalValueBindingObserverBase<T> instance, BindingValue<T> value)
{
var owner = instance._owner;
var property = instance.Property;
var value = BindingValue<T>.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.

2
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -104,7 +104,7 @@ namespace Avalonia.PropertyStore
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueUntypedBindingObserver<T>(this, property);
var observer = new LocalValueBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;

Loading…
Cancel
Save