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.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class UntypedBindingEntry<T> : IValueEntry<T>, |
|||
IObserver<object?>, |
|||
IDisposable |
|||
/// <summary>
|
|||
/// An <see cref="IValueEntry"/> that holds a binding whose source observable and target
|
|||
/// property are both untyped.
|
|||
/// </summary>
|
|||
internal class UntypedBindingEntry : BindingEntryBase<object?, object?> |
|||
{ |
|||
private static IDisposable s_Creating = Disposable.Empty; |
|||
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; |
|||
private readonly Func<object?, bool>? _validate; |
|||
|
|||
public UntypedBindingEntry( |
|||
ValueFrame frame, |
|||
StyledPropertyBase<T> property, |
|||
AvaloniaProperty property, |
|||
IObservable<object?> source) |
|||
: base(frame, property, source) |
|||
{ |
|||
_frame = frame; |
|||
_source = source; |
|||
Property = property; |
|||
_validate = ((IStyledPropertyAccessor)property).ValidateValue; |
|||
} |
|||
|
|||
public bool HasValue |
|||
protected override BindingValue<object?> ConvertAndValidate(object? value) |
|||
{ |
|||
get |
|||
{ |
|||
Start(produceValue: false); |
|||
return _hasValue; |
|||
} |
|||
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate); |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
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) |
|||
protected override BindingValue<object?> ConvertAndValidate(BindingValue<object?> value) |
|||
{ |
|||
if (_subscription is not null) |
|||
return; |
|||
_subscription = produceValue ? s_Creating : s_CreatingQuiet; |
|||
_subscription = _source.Subscribe(this); |
|||
throw new NotSupportedException(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue