Browse Source
Too many gotchas in implementing binding entries, we'll take the performance hit of a few virtual method calls.pull/8600/head
11 changed files with 343 additions and 465 deletions
@ -1,136 +0,0 @@ |
|||||
using System; |
|
||||
using System.Reactive.Disposables; |
|
||||
using Avalonia.Data; |
|
||||
|
|
||||
namespace Avalonia.PropertyStore |
|
||||
{ |
|
||||
internal class BindingEntry : IValueEntry, |
|
||||
IObserver<object?>, |
|
||||
IDisposable |
|
||||
{ |
|
||||
private static IDisposable s_Creating = Disposable.Empty; |
|
||||
private static IDisposable s_CreatingQuiet = Disposable.Create(() => { }); |
|
||||
private readonly ValueFrame _frame; |
|
||||
private IDisposable? _subscription; |
|
||||
private bool _hasValue; |
|
||||
private object? _value; |
|
||||
|
|
||||
public BindingEntry( |
|
||||
ValueFrame frame, |
|
||||
AvaloniaProperty property, |
|
||||
IObservable<object?> source) |
|
||||
{ |
|
||||
_frame = frame; |
|
||||
Source = source; |
|
||||
Property = property; |
|
||||
} |
|
||||
|
|
||||
public bool HasValue |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
Start(produceValue: false); |
|
||||
return _hasValue; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public AvaloniaProperty Property { get; } |
|
||||
protected IObservable<object?> Source { get; } |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
Unsubscribe(); |
|
||||
BindingCompleted(); |
|
||||
} |
|
||||
|
|
||||
public object? GetValue() |
|
||||
{ |
|
||||
Start(produceValue: false); |
|
||||
if (!_hasValue) |
|
||||
throw new AvaloniaInternalException("The binding entry has no value."); |
|
||||
return _value!; |
|
||||
} |
|
||||
|
|
||||
public bool TryGetValue(out object? value) |
|
||||
{ |
|
||||
Start(produceValue: false); |
|
||||
value = _value; |
|
||||
return _hasValue; |
|
||||
} |
|
||||
|
|
||||
public void Start() => Start(true); |
|
||||
public void OnCompleted() => BindingCompleted(); |
|
||||
public void OnError(Exception error) => BindingCompleted(); |
|
||||
|
|
||||
public void OnNext(object? value) => SetValue(value); |
|
||||
|
|
||||
public virtual void Unsubscribe() |
|
||||
{ |
|
||||
_subscription?.Dispose(); |
|
||||
_subscription = null; |
|
||||
} |
|
||||
|
|
||||
protected virtual void Start(bool produceValue) |
|
||||
{ |
|
||||
if (_subscription is not null) |
|
||||
return; |
|
||||
_subscription = produceValue ? s_Creating : s_CreatingQuiet; |
|
||||
_subscription = Source.Subscribe(this); |
|
||||
} |
|
||||
|
|
||||
private void ClearValue() |
|
||||
{ |
|
||||
if (_hasValue) |
|
||||
{ |
|
||||
_hasValue = false; |
|
||||
_value = default; |
|
||||
|
|
||||
if (_subscription is not null && _subscription != s_CreatingQuiet) |
|
||||
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void SetValue(object? value) |
|
||||
{ |
|
||||
if (_frame.Owner is null) |
|
||||
return; |
|
||||
|
|
||||
if (value is BindingNotification n) |
|
||||
{ |
|
||||
value = n.Value; |
|
||||
LoggingUtils.LogIfNecessary(_frame.Owner.Owner, Property, n); |
|
||||
} |
|
||||
|
|
||||
if (value == AvaloniaProperty.UnsetValue) |
|
||||
{ |
|
||||
ClearValue(); |
|
||||
} |
|
||||
else if (value == BindingOperations.DoNothing) |
|
||||
{ |
|
||||
// Do nothing!
|
|
||||
} |
|
||||
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) |
|
||||
{ |
|
||||
if (!_hasValue || !Equals(_value, typedValue)) |
|
||||
{ |
|
||||
_value = typedValue; |
|
||||
_hasValue = true; |
|
||||
|
|
||||
if (_subscription is not null && _subscription != s_CreatingQuiet) |
|
||||
_frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, typedValue); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ClearValue(); |
|
||||
LoggingUtils.LogInvalidValue(_frame.Owner.Owner, Property, Property.PropertyType, value); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void BindingCompleted() |
|
||||
{ |
|
||||
_subscription = null; |
|
||||
_frame.OnBindingCompleted(this); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,36 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Data; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IValueEntry"/> that holds a binding whose source observable is untyped and
|
||||
|
/// target property is typed.
|
||||
|
/// </summary>
|
||||
|
internal sealed class SourceUntypedBindingEntry<TTarget> : BindingEntryBase<TTarget, object?>, |
||||
|
IValueEntry<TTarget> |
||||
|
{ |
||||
|
private readonly Func<TTarget, bool>? _validate; |
||||
|
|
||||
|
public SourceUntypedBindingEntry( |
||||
|
ValueFrame frame, |
||||
|
StyledPropertyBase<TTarget> property, |
||||
|
IObservable<object?> source) |
||||
|
: base(frame, property, source) |
||||
|
{ |
||||
|
_validate = property.ValidateValue; |
||||
|
} |
||||
|
|
||||
|
public new StyledPropertyBase<TTarget> Property => (StyledPropertyBase<TTarget>)base.Property; |
||||
|
|
||||
|
protected override BindingValue<TTarget> ConvertAndValidate(object? value) |
||||
|
{ |
||||
|
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate); |
||||
|
} |
||||
|
|
||||
|
protected override BindingValue<TTarget> ConvertAndValidate(BindingValue<object?> value) |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Data; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IValueEntry"/> that holds a binding whose source observable and target
|
||||
|
/// property are both typed.
|
||||
|
/// </summary>
|
||||
|
internal sealed class TypedBindingEntry<T> : BindingEntryBase<T, T>, IValueEntry<T> |
||||
|
{ |
||||
|
public TypedBindingEntry( |
||||
|
ValueFrame frame, |
||||
|
StyledPropertyBase<T> property, |
||||
|
IObservable<T> source) |
||||
|
: base(frame, property, source) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public TypedBindingEntry( |
||||
|
ValueFrame frame, |
||||
|
StyledPropertyBase<T> property, |
||||
|
IObservable<BindingValue<T>> source) |
||||
|
: base(frame, property, source) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public new StyledPropertyBase<T> Property => (StyledPropertyBase<T>)base.Property; |
||||
|
|
||||
|
protected override BindingValue<T> ConvertAndValidate(T value) |
||||
|
{ |
||||
|
if (Property.ValidateValue?.Invoke(value) == false) |
||||
|
{ |
||||
|
return BindingValue<T>.BindingError( |
||||
|
new InvalidCastException($"'{value}' is not a valid value.")); |
||||
|
} |
||||
|
|
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
protected override BindingValue<T> ConvertAndValidate(BindingValue<T> value) |
||||
|
{ |
||||
|
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) |
||||
|
{ |
||||
|
return BindingValue<T>.BindingError( |
||||
|
new InvalidCastException($"'{value.Value}' is not a valid value.")); |
||||
|
} |
||||
|
|
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,163 +1,33 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using System.Reactive.Disposables; |
|
||||
using Avalonia.Data; |
using Avalonia.Data; |
||||
|
|
||||
namespace Avalonia.PropertyStore |
namespace Avalonia.PropertyStore |
||||
{ |
{ |
||||
internal class UntypedBindingEntry<T> : IValueEntry<T>, |
/// <summary>
|
||||
IObserver<object?>, |
/// An <see cref="IValueEntry"/> that holds a binding whose source observable and target
|
||||
IDisposable |
/// property are both untyped.
|
||||
|
/// </summary>
|
||||
|
internal class UntypedBindingEntry : BindingEntryBase<object?, object?> |
||||
{ |
{ |
||||
private static IDisposable s_Creating = Disposable.Empty; |
private readonly Func<object?, bool>? _validate; |
||||
private static IDisposable s_CreatingQuiet = Disposable.Create(() => { }); |
|
||||
private readonly ValueFrame _frame; |
|
||||
private readonly IObservable<object?> _source; |
|
||||
private IDisposable? _subscription; |
|
||||
private bool _hasValue; |
|
||||
private T? _value; |
|
||||
|
|
||||
public UntypedBindingEntry( |
public UntypedBindingEntry( |
||||
ValueFrame frame, |
ValueFrame frame, |
||||
StyledPropertyBase<T> property, |
AvaloniaProperty property, |
||||
IObservable<object?> source) |
IObservable<object?> source) |
||||
|
: base(frame, property, source) |
||||
{ |
{ |
||||
_frame = frame; |
_validate = ((IStyledPropertyAccessor)property).ValidateValue; |
||||
_source = source; |
|
||||
Property = property; |
|
||||
} |
} |
||||
|
|
||||
public bool HasValue |
protected override BindingValue<object?> ConvertAndValidate(object? value) |
||||
{ |
{ |
||||
get |
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate); |
||||
{ |
|
||||
Start(produceValue: false); |
|
||||
return _hasValue; |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
public StyledPropertyBase<T> Property { get; } |
protected override BindingValue<object?> ConvertAndValidate(BindingValue<object?> value) |
||||
AvaloniaProperty IValueEntry.Property => Property; |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
Unsubscribe(); |
|
||||
BindingCompleted(); |
|
||||
} |
|
||||
|
|
||||
public T GetValue() |
|
||||
{ |
|
||||
Start(produceValue: false); |
|
||||
if (!_hasValue) |
|
||||
throw new AvaloniaInternalException("The binding entry has no value."); |
|
||||
return _value!; |
|
||||
} |
|
||||
|
|
||||
public void Start() => Start(true); |
|
||||
|
|
||||
public bool TryGetValue([MaybeNullWhen(false)] out T value) |
|
||||
{ |
|
||||
Start(produceValue: false); |
|
||||
value = _value; |
|
||||
return _hasValue; |
|
||||
} |
|
||||
|
|
||||
public void OnCompleted() => BindingCompleted(); |
|
||||
public void OnError(Exception error) => BindingCompleted(); |
|
||||
|
|
||||
public void OnNext(object? value) => SetValue(value); |
|
||||
|
|
||||
public void OnNext(BindingValue<T> value) |
|
||||
{ |
|
||||
if (value.HasValue) |
|
||||
SetValue(value.Value); |
|
||||
else |
|
||||
ClearValue(); |
|
||||
} |
|
||||
|
|
||||
public void Unsubscribe() |
|
||||
{ |
|
||||
_subscription?.Dispose(); |
|
||||
_subscription = null; |
|
||||
} |
|
||||
|
|
||||
object? IValueEntry.GetValue() |
|
||||
{ |
|
||||
Start(produceValue: false); |
|
||||
if (!_hasValue) |
|
||||
throw new AvaloniaInternalException("The BindingEntry<T> has no value."); |
|
||||
return _value!; |
|
||||
} |
|
||||
|
|
||||
bool IValueEntry.TryGetValue(out object? value) |
|
||||
{ |
|
||||
Start(produceValue: false); |
|
||||
value = _value; |
|
||||
return _hasValue; |
|
||||
} |
|
||||
|
|
||||
private void ClearValue() |
|
||||
{ |
|
||||
if (_hasValue) |
|
||||
{ |
|
||||
_hasValue = false; |
|
||||
_value = default; |
|
||||
|
|
||||
if (_subscription is not null && _subscription != s_CreatingQuiet) |
|
||||
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void SetValue(object? value) |
|
||||
{ |
|
||||
if (_frame.Owner is null) |
|
||||
return; |
|
||||
|
|
||||
if (value is BindingNotification n) |
|
||||
{ |
|
||||
value = n.Value; |
|
||||
LoggingUtils.LogIfNecessary(_frame.Owner.Owner, Property, n); |
|
||||
} |
|
||||
|
|
||||
if (value == AvaloniaProperty.UnsetValue) |
|
||||
{ |
|
||||
ClearValue(); |
|
||||
} |
|
||||
else if (value == BindingOperations.DoNothing) |
|
||||
{ |
|
||||
// Do nothing!
|
|
||||
} |
|
||||
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) |
|
||||
{ |
|
||||
if (!_hasValue || !EqualityComparer<T>.Default.Equals(_value, typedValue)) |
|
||||
{ |
|
||||
_value = typedValue; |
|
||||
_hasValue = true; |
|
||||
|
|
||||
if (_subscription is not null && _subscription != s_CreatingQuiet) |
|
||||
_frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, typedValue); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ClearValue(); |
|
||||
LoggingUtils.LogInvalidValue(_frame.Owner.Owner, Property, typeof(T), value); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void BindingCompleted() |
|
||||
{ |
|
||||
_subscription = null; |
|
||||
_frame.OnBindingCompleted(this); |
|
||||
} |
|
||||
|
|
||||
private void Start(bool produceValue) |
|
||||
{ |
{ |
||||
if (_subscription is not null) |
throw new NotSupportedException(); |
||||
return; |
|
||||
_subscription = produceValue ? s_Creating : s_CreatingQuiet; |
|
||||
_subscription = _source.Subscribe(this); |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue