committed by
GitHub
118 changed files with 6348 additions and 4455 deletions
@ -0,0 +1,32 @@ |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace System; |
|||
|
|||
#if !NET6_0_OR_GREATER
|
|||
internal static class CollectionCompatibilityExtensions |
|||
{ |
|||
public static bool Remove<TKey, TValue>( |
|||
this Dictionary<TKey, TValue> o, |
|||
TKey key, |
|||
[MaybeNullWhen(false)] out TValue value) |
|||
where TKey : notnull |
|||
{ |
|||
if (o.TryGetValue(key, out value)) |
|||
return o.Remove(key); |
|||
return false; |
|||
} |
|||
|
|||
public static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> o, TKey key, TValue value) |
|||
where TKey : notnull |
|||
{ |
|||
if (!o.ContainsKey(key)) |
|||
{ |
|||
o.Add(key, value); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
#endif
|
|||
@ -0,0 +1,25 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal static class AvaloniaPropertyDictionaryPool<TValue> |
|||
{ |
|||
private const int MaxPoolSize = 4; |
|||
private static readonly Stack<AvaloniaPropertyDictionary<TValue>> _pool = new(); |
|||
|
|||
public static AvaloniaPropertyDictionary<TValue> Get() |
|||
{ |
|||
return _pool.Count == 0 ? new() : _pool.Pop(); |
|||
} |
|||
|
|||
public static void Release(AvaloniaPropertyDictionary<TValue> dictionary) |
|||
{ |
|||
if (_pool.Count < MaxPoolSize) |
|||
{ |
|||
dictionary.Clear(); |
|||
_pool.Push(dictionary); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,154 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
|
|||
/// </summary>
|
|||
internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable |
|||
{ |
|||
void Start(bool ignoreBatchUpdate); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stores a binding in a <see cref="ValueStore"/> or <see cref="PriorityValue{T}"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>> |
|||
{ |
|||
private readonly AvaloniaObject _owner; |
|||
private ValueOwner<T> _sink; |
|||
private IDisposable? _subscription; |
|||
private bool _isSubscribed; |
|||
private bool _batchUpdate; |
|||
private Optional<T> _value; |
|||
|
|||
public BindingEntry( |
|||
AvaloniaObject owner, |
|||
StyledPropertyBase<T> property, |
|||
IObservable<BindingValue<T>> source, |
|||
BindingPriority priority, |
|||
ValueOwner<T> sink) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
Source = source; |
|||
Priority = priority; |
|||
_sink = sink; |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
public BindingPriority Priority { get; private set; } |
|||
public IObservable<BindingValue<T>> Source { get; } |
|||
Optional<object?> IValue.GetValue() => _value.ToObject(); |
|||
|
|||
public void BeginBatchUpdate() => _batchUpdate = true; |
|||
|
|||
public void EndBatchUpdate() |
|||
{ |
|||
_batchUpdate = false; |
|||
|
|||
if (_sink.IsValueStore) |
|||
Start(); |
|||
} |
|||
|
|||
public Optional<T> GetValue(BindingPriority maxPriority) |
|||
{ |
|||
return Priority >= maxPriority ? _value : Optional<T>.Empty; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
OnCompleted(); |
|||
} |
|||
|
|||
public void OnCompleted() |
|||
{ |
|||
var oldValue = _value; |
|||
_value = default; |
|||
Priority = BindingPriority.Unset; |
|||
_isSubscribed = false; |
|||
_sink.Completed(Property, this, oldValue); |
|||
} |
|||
|
|||
public void OnError(Exception error) |
|||
{ |
|||
throw new NotImplementedException("BindingEntry.OnError is not implemented", error); |
|||
} |
|||
|
|||
public void OnNext(BindingValue<T> 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() => Start(false); |
|||
|
|||
public void Start(bool ignoreBatchUpdate) |
|||
{ |
|||
// We can't use _subscription to check whether we're subscribed because it won't be set
|
|||
// until Subscribe has finished, which will be too late to prevent reentrancy. In addition
|
|||
// don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
|
|||
if (!_isSubscribed && |
|||
Priority != BindingPriority.Unset && |
|||
(!_batchUpdate || ignoreBatchUpdate)) |
|||
{ |
|||
_isSubscribed = true; |
|||
_subscription = Source.Subscribe(this); |
|||
} |
|||
} |
|||
|
|||
public void Reparent(PriorityValue<T> parent) => _sink = new(parent); |
|||
|
|||
public void RaiseValueChanged( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
Optional<object?> oldValue, |
|||
Optional<object?> newValue) |
|||
{ |
|||
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( |
|||
owner, |
|||
(AvaloniaProperty<T>)property, |
|||
oldValue.Cast<T>(), |
|||
newValue.Cast<T>(), |
|||
Priority)); |
|||
} |
|||
|
|||
private void UpdateValue(BindingValue<T> value) |
|||
{ |
|||
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) |
|||
{ |
|||
value = Property.GetDefaultValue(_owner.GetType()); |
|||
} |
|||
|
|||
if (value.Type == BindingValueType.DoNothing) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var old = _value; |
|||
|
|||
if (value.Type != BindingValueType.DataValidationError) |
|||
{ |
|||
_value = value.ToOptional(); |
|||
} |
|||
|
|||
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(_owner, Property, old, value, Priority)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal abstract class BindingEntryBase<TValue, TSource> : IValueEntry<TValue>, |
|||
IObserver<TSource>, |
|||
IObserver<BindingValue<TSource>>, |
|||
IDisposable |
|||
{ |
|||
private static readonly IDisposable s_creating = Disposable.Empty; |
|||
private static readonly IDisposable s_creatingQuiet = Disposable.Create(() => { }); |
|||
private IDisposable? _subscription; |
|||
private bool _hasValue; |
|||
private TValue? _value; |
|||
|
|||
protected BindingEntryBase( |
|||
ValueFrame frame, |
|||
AvaloniaProperty property, |
|||
IObservable<BindingValue<TSource>> source) |
|||
{ |
|||
Frame = frame; |
|||
Source = source; |
|||
Property = property; |
|||
} |
|||
|
|||
protected BindingEntryBase( |
|||
ValueFrame frame, |
|||
AvaloniaProperty property, |
|||
IObservable<TSource> source) |
|||
{ |
|||
Frame = frame; |
|||
Source = source; |
|||
Property = property; |
|||
} |
|||
|
|||
public bool HasValue |
|||
{ |
|||
get |
|||
{ |
|||
Start(produceValue: false); |
|||
return _hasValue; |
|||
} |
|||
} |
|||
|
|||
public bool IsSubscribed => _subscription is not null; |
|||
public AvaloniaProperty Property { get; } |
|||
AvaloniaProperty IValueEntry.Property => Property; |
|||
protected ValueFrame Frame { get; } |
|||
protected object Source { get; } |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Unsubscribe(); |
|||
BindingCompleted(); |
|||
} |
|||
|
|||
public TValue GetValue() |
|||
{ |
|||
Start(produceValue: false); |
|||
if (!_hasValue) |
|||
throw new AvaloniaInternalException("The binding entry has no value."); |
|||
return _value!; |
|||
} |
|||
|
|||
public void Start() => Start(true); |
|||
|
|||
public void OnCompleted() => BindingCompleted(); |
|||
public void OnError(Exception error) => BindingCompleted(); |
|||
public void OnNext(TSource value) => SetValue(ConvertAndValidate(value)); |
|||
public void OnNext(BindingValue<TSource> value) => SetValue(ConvertAndValidate(value)); |
|||
|
|||
public virtual 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!; |
|||
} |
|||
|
|||
protected abstract BindingValue<TValue> ConvertAndValidate(TSource value); |
|||
protected abstract BindingValue<TValue> ConvertAndValidate(BindingValue<TSource> value); |
|||
|
|||
protected virtual void Start(bool produceValue) |
|||
{ |
|||
if (_subscription is not null) |
|||
return; |
|||
|
|||
_subscription = produceValue ? s_creating : s_creatingQuiet; |
|||
_subscription = Source switch |
|||
{ |
|||
IObservable<BindingValue<TSource>> bv => bv.Subscribe(this), |
|||
IObservable<TSource> b => b.Subscribe(this), |
|||
_ => throw new AvaloniaInternalException("Unexpected binding source."), |
|||
}; |
|||
} |
|||
|
|||
private void ClearValue() |
|||
{ |
|||
if (_hasValue) |
|||
{ |
|||
_hasValue = false; |
|||
_value = default; |
|||
if (_subscription is not null) |
|||
Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); |
|||
} |
|||
} |
|||
|
|||
private void SetValue(BindingValue<TValue> value) |
|||
{ |
|||
if (Frame.Owner is null) |
|||
return; |
|||
|
|||
LoggingUtils.LogIfNecessary(Frame.Owner.Owner, Property, value); |
|||
|
|||
if (value.HasValue) |
|||
{ |
|||
if (!_hasValue || !EqualityComparer<TValue>.Default.Equals(_value, value.Value)) |
|||
{ |
|||
_value = value.Value; |
|||
_hasValue = true; |
|||
if (_subscription is not null && _subscription != s_creatingQuiet) |
|||
Frame.Owner?.OnBindingValueChanged(this, Frame.Priority); |
|||
} |
|||
} |
|||
else if (value.Type != BindingValueType.DoNothing) |
|||
{ |
|||
ClearValue(); |
|||
if (_subscription is not null && _subscription != s_creatingQuiet) |
|||
Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); |
|||
} |
|||
} |
|||
|
|||
private void BindingCompleted() |
|||
{ |
|||
_subscription = null; |
|||
Frame.OnBindingCompleted(this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,82 +0,0 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
|
|||
/// </summary>
|
|||
internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stores a value with a priority in a <see cref="ValueStore"/> or
|
|||
/// <see cref="PriorityValue{T}"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry |
|||
{ |
|||
private ValueOwner<T> _sink; |
|||
private Optional<T> _value; |
|||
|
|||
public ConstantValueEntry( |
|||
StyledPropertyBase<T> property, |
|||
T value, |
|||
BindingPriority priority, |
|||
ValueOwner<T> sink) |
|||
{ |
|||
Property = property; |
|||
_value = value; |
|||
Priority = priority; |
|||
_sink = sink; |
|||
} |
|||
|
|||
public ConstantValueEntry( |
|||
StyledPropertyBase<T> property, |
|||
Optional<T> value, |
|||
BindingPriority priority, |
|||
ValueOwner<T> sink) |
|||
{ |
|||
Property = property; |
|||
_value = value; |
|||
Priority = priority; |
|||
_sink = sink; |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
public BindingPriority Priority { get; private set; } |
|||
Optional<object?> IValue.GetValue() => _value.ToObject(); |
|||
|
|||
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation) |
|||
{ |
|||
return Priority >= maxPriority ? _value : Optional<T>.Empty; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
var oldValue = _value; |
|||
_value = default; |
|||
Priority = BindingPriority.Unset; |
|||
_sink.Completed(Property, this, oldValue); |
|||
} |
|||
|
|||
public void Reparent(PriorityValue<T> sink) => _sink = new(sink); |
|||
public void Start() { } |
|||
|
|||
public void RaiseValueChanged( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
Optional<object?> oldValue, |
|||
Optional<object?> newValue) |
|||
{ |
|||
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( |
|||
owner, |
|||
(AvaloniaProperty<T>)property, |
|||
oldValue.Cast<T>(), |
|||
newValue.Cast<T>(), |
|||
Priority)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class DirectBindingObserver<T> : IObserver<T>, |
|||
IObserver<BindingValue<T>>, |
|||
IDisposable |
|||
{ |
|||
private readonly ValueStore _owner; |
|||
private IDisposable? _subscription; |
|||
|
|||
public DirectBindingObserver(ValueStore owner, DirectPropertyBase<T> property) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
} |
|||
|
|||
public DirectPropertyBase<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 OnNext(T value) |
|||
{ |
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
_owner.Owner.SetDirectValueUnchecked<T>(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.Owner; |
|||
var property = Property; |
|||
var newValue = value; |
|||
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); |
|||
} |
|||
} |
|||
|
|||
public void OnNext(BindingValue<T> value) |
|||
{ |
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
_owner.Owner.SetDirectValueUnchecked<T>(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.Owner; |
|||
var property = Property; |
|||
var newValue = value; |
|||
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class DirectUntypedBindingObserver<T> : IObserver<object?>, |
|||
IDisposable |
|||
{ |
|||
private readonly ValueStore _owner; |
|||
private IDisposable? _subscription; |
|||
|
|||
public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase<T> property) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
} |
|||
|
|||
public DirectPropertyBase<T> Property { get;} |
|||
|
|||
public void Start(IObservable<object?> 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 OnNext(object? value) |
|||
{ |
|||
var typed = BindingValue<T>.FromUntyped(value); |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
_owner.Owner.SetDirectValueUnchecked<T>(Property, typed); |
|||
} |
|||
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.Owner; |
|||
var property = Property; |
|||
var newValue = value; |
|||
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, typed)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,168 @@ |
|||
using System.Diagnostics; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents the active value for a property in a <see cref="ValueStore"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This class is an abstract base for the generic <see cref="EffectiveValue{T}"/>.
|
|||
/// </remarks>
|
|||
internal abstract class EffectiveValue |
|||
{ |
|||
private IValueEntry? _valueEntry; |
|||
private IValueEntry? _baseValueEntry; |
|||
|
|||
/// <summary>
|
|||
/// Gets the current effective value as a boxed value.
|
|||
/// </summary>
|
|||
public object? Value => GetBoxedValue(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the priority of the current effective value.
|
|||
/// </summary>
|
|||
public BindingPriority Priority { get; protected set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the priority of the current base value.
|
|||
/// </summary>
|
|||
public BindingPriority BasePriority { get; protected set; } |
|||
|
|||
/// <summary>
|
|||
/// Begins a reevaluation pass on the effective value.
|
|||
/// </summary>
|
|||
/// <param name="clearLocalValue">
|
|||
/// Determines whether any current local value should be cleared.
|
|||
/// </param>
|
|||
/// <remarks>
|
|||
/// This method resets the <see cref="Priority"/> and <see cref="BasePriority"/> properties
|
|||
/// to Unset, pending reevaluation.
|
|||
/// </remarks>
|
|||
public void BeginReevaluation(bool clearLocalValue = false) |
|||
{ |
|||
if (clearLocalValue || Priority != BindingPriority.LocalValue) |
|||
Priority = BindingPriority.Unset; |
|||
if (clearLocalValue || BasePriority != BindingPriority.LocalValue) |
|||
BasePriority = BindingPriority.Unset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Ends a reevaluation pass on the effective value.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method unsubscribes from any unused value entries.
|
|||
/// </remarks>
|
|||
public void EndReevaluation() |
|||
{ |
|||
if (Priority == BindingPriority.Unset) |
|||
{ |
|||
_valueEntry?.Unsubscribe(); |
|||
_valueEntry = null; |
|||
} |
|||
|
|||
if (BasePriority == BindingPriority.Unset) |
|||
{ |
|||
_baseValueEntry?.Unsubscribe(); |
|||
_baseValueEntry = null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the value and base value for a non-LocalValue priority, raising
|
|||
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
|
|||
/// </summary>
|
|||
/// <param name="owner">The associated value store.</param>
|
|||
/// <param name="value">The new value of the property.</param>
|
|||
/// <param name="priority">The priority of the new value.</param>
|
|||
public abstract void SetAndRaise( |
|||
ValueStore owner, |
|||
IValueEntry value, |
|||
BindingPriority priority); |
|||
|
|||
/// <summary>
|
|||
/// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value
|
|||
/// change.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner object.</param>
|
|||
/// <param name="property">The property being changed.</param>
|
|||
/// <param name="oldValue">The old value of the property.</param>
|
|||
/// <param name="newValue">The new value of the property.</param>
|
|||
public abstract void RaiseInheritedValueChanged( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
EffectiveValue? oldValue, |
|||
EffectiveValue? newValue); |
|||
|
|||
/// <summary>
|
|||
/// Removes the current animation value and reverts to the base value, raising
|
|||
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
|
|||
/// </summary>
|
|||
/// <param name="owner">The associated value store.</param>
|
|||
/// <param name="property">The property being changed.</param>
|
|||
public abstract void RemoveAnimationAndRaise(ValueStore owner, AvaloniaProperty property); |
|||
|
|||
/// <summary>
|
|||
/// Coerces the property value.
|
|||
/// </summary>
|
|||
/// <param name="owner">The associated value store.</param>
|
|||
/// <param name="property">The property to coerce.</param>
|
|||
public abstract void CoerceValue(ValueStore owner, AvaloniaProperty property); |
|||
|
|||
/// <summary>
|
|||
/// Disposes the effective value, raising <see cref="AvaloniaObject.PropertyChanged"/>
|
|||
/// where necessary.
|
|||
/// </summary>
|
|||
/// <param name="owner">The associated value store.</param>
|
|||
/// <param name="property">The property being cleared.</param>
|
|||
public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property); |
|||
|
|||
protected abstract object? GetBoxedValue(); |
|||
|
|||
protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority) |
|||
{ |
|||
Debug.Assert(priority != BindingPriority.LocalValue); |
|||
|
|||
if (priority <= BindingPriority.Animation) |
|||
{ |
|||
// If we've received an animation value and the current value is a non-animation
|
|||
// value, then the current entry becomes our base entry.
|
|||
if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited) |
|||
{ |
|||
Debug.Assert(_valueEntry is not null); |
|||
_baseValueEntry = _valueEntry; |
|||
_valueEntry = null; |
|||
} |
|||
|
|||
if (_valueEntry != entry) |
|||
{ |
|||
_valueEntry?.Unsubscribe(); |
|||
_valueEntry = entry; |
|||
} |
|||
} |
|||
else if (Priority <= BindingPriority.Animation) |
|||
{ |
|||
// We've received a non-animation value and have an active animation value, so the
|
|||
// new entry becomes our base entry.
|
|||
if (_baseValueEntry != entry) |
|||
{ |
|||
_baseValueEntry?.Unsubscribe(); |
|||
_baseValueEntry = entry; |
|||
} |
|||
} |
|||
else if (_valueEntry != entry) |
|||
{ |
|||
// Both the current value and the new value are non-animation values, so the new
|
|||
// entry replaces the existing entry.
|
|||
_valueEntry?.Unsubscribe(); |
|||
_valueEntry = entry; |
|||
} |
|||
} |
|||
|
|||
protected void UnsubscribeValueEntries() |
|||
{ |
|||
_valueEntry?.Unsubscribe(); |
|||
_baseValueEntry?.Unsubscribe(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,270 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents the active value for a property in a <see cref="ValueStore"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Stores the active value in an <see cref="AvaloniaObject"/>'s <see cref="ValueStore"/>
|
|||
/// for a single property, when the value is not inherited or unset/default.
|
|||
/// </remarks>
|
|||
internal sealed class EffectiveValue<T> : EffectiveValue |
|||
{ |
|||
private readonly StyledPropertyMetadata<T> _metadata; |
|||
private T? _baseValue; |
|||
private UncommonFields? _uncommon; |
|||
|
|||
public EffectiveValue(AvaloniaObject owner, StyledPropertyBase<T> property) |
|||
{ |
|||
Priority = BindingPriority.Unset; |
|||
BasePriority = BindingPriority.Unset; |
|||
_metadata = property.GetMetadata(owner.GetType()); |
|||
|
|||
var value = _metadata.DefaultValue; |
|||
|
|||
if (property.HasCoercion && _metadata.CoerceValue is { } coerce) |
|||
{ |
|||
_uncommon = new() |
|||
{ |
|||
_coerce = coerce, |
|||
_uncoercedValue = value, |
|||
_uncoercedBaseValue = value, |
|||
}; |
|||
|
|||
Value = coerce(owner, value); |
|||
} |
|||
else |
|||
{ |
|||
Value = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the current effective value.
|
|||
/// </summary>
|
|||
public new T Value { get; private set; } |
|||
|
|||
public override void SetAndRaise( |
|||
ValueStore owner, |
|||
IValueEntry value, |
|||
BindingPriority priority) |
|||
{ |
|||
Debug.Assert(priority != BindingPriority.LocalValue); |
|||
UpdateValueEntry(value, priority); |
|||
|
|||
SetAndRaiseCore(owner, (StyledPropertyBase<T>)value.Property, GetValue(value), priority); |
|||
} |
|||
|
|||
public void SetLocalValueAndRaise( |
|||
ValueStore owner, |
|||
StyledPropertyBase<T> property, |
|||
T value) |
|||
{ |
|||
SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); |
|||
} |
|||
|
|||
public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) |
|||
{ |
|||
value = _baseValue!; |
|||
return BasePriority != BindingPriority.Unset; |
|||
} |
|||
|
|||
public override void RaiseInheritedValueChanged( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
EffectiveValue? oldValue, |
|||
EffectiveValue? newValue) |
|||
{ |
|||
Debug.Assert(oldValue is not null || newValue is not null); |
|||
|
|||
var p = (StyledPropertyBase<T>)property; |
|||
var o = oldValue is not null ? ((EffectiveValue<T>)oldValue).Value : _metadata.DefaultValue; |
|||
var n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : _metadata.DefaultValue; |
|||
var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset; |
|||
|
|||
if (!EqualityComparer<T>.Default.Equals(o, n)) |
|||
{ |
|||
owner.RaisePropertyChanged(p, o, n, priority, true); |
|||
} |
|||
} |
|||
|
|||
public override void RemoveAnimationAndRaise(ValueStore owner, AvaloniaProperty property) |
|||
{ |
|||
Debug.Assert(Priority != BindingPriority.Animation); |
|||
Debug.Assert(BasePriority != BindingPriority.Unset); |
|||
UpdateValueEntry(null, BindingPriority.Animation); |
|||
SetAndRaiseCore(owner, (StyledPropertyBase<T>)property, _baseValue!, BasePriority); |
|||
} |
|||
|
|||
public override void CoerceValue(ValueStore owner, AvaloniaProperty property) |
|||
{ |
|||
if (_uncommon is null) |
|||
return; |
|||
SetAndRaiseCore( |
|||
owner, |
|||
(StyledPropertyBase<T>)property, |
|||
_uncommon._uncoercedValue!, |
|||
Priority, |
|||
_uncommon._uncoercedBaseValue!, |
|||
BasePriority); |
|||
} |
|||
|
|||
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) |
|||
{ |
|||
UnsubscribeValueEntries(); |
|||
DisposeAndRaiseUnset(owner, (StyledPropertyBase<T>)property); |
|||
} |
|||
|
|||
public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase<T> property) |
|||
{ |
|||
BindingPriority priority; |
|||
T oldValue; |
|||
|
|||
if (property.Inherits && owner.TryGetInheritedValue(property, out var i)) |
|||
{ |
|||
oldValue = ((EffectiveValue<T>)i).Value; |
|||
priority = BindingPriority.Inherited; |
|||
} |
|||
else |
|||
{ |
|||
oldValue = _metadata.DefaultValue; |
|||
priority = BindingPriority.Unset; |
|||
} |
|||
|
|||
if (!EqualityComparer<T>.Default.Equals(oldValue, Value)) |
|||
{ |
|||
owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true); |
|||
if (property.Inherits) |
|||
owner.OnInheritedEffectiveValueDisposed(property, Value); |
|||
} |
|||
} |
|||
|
|||
protected override object? GetBoxedValue() => Value; |
|||
|
|||
private static T GetValue(IValueEntry entry) |
|||
{ |
|||
if (entry is IValueEntry<T> typed) |
|||
return typed.GetValue(); |
|||
else |
|||
return (T)entry.GetValue()!; |
|||
} |
|||
|
|||
private void SetAndRaiseCore( |
|||
ValueStore owner, |
|||
StyledPropertyBase<T> property, |
|||
T value, |
|||
BindingPriority priority) |
|||
{ |
|||
Debug.Assert(priority < BindingPriority.Inherited); |
|||
|
|||
var oldValue = Value; |
|||
var valueChanged = false; |
|||
var baseValueChanged = false; |
|||
var v = value; |
|||
|
|||
if (_uncommon?._coerce is { } coerce) |
|||
v = coerce(owner.Owner, value); |
|||
|
|||
if (priority <= Priority) |
|||
{ |
|||
valueChanged = !EqualityComparer<T>.Default.Equals(Value, v); |
|||
Value = v; |
|||
Priority = priority; |
|||
if (_uncommon is not null) |
|||
_uncommon._uncoercedValue = value; |
|||
} |
|||
|
|||
if (priority <= BasePriority && priority >= BindingPriority.LocalValue) |
|||
{ |
|||
baseValueChanged = !EqualityComparer<T>.Default.Equals(_baseValue, v); |
|||
_baseValue = v; |
|||
BasePriority = priority; |
|||
if (_uncommon is not null) |
|||
_uncommon._uncoercedBaseValue = value; |
|||
} |
|||
|
|||
if (valueChanged) |
|||
{ |
|||
using var notifying = PropertyNotifying.Start(owner.Owner, property); |
|||
owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true); |
|||
if (property.Inherits) |
|||
owner.OnInheritedEffectiveValueChanged(property, oldValue, this); |
|||
} |
|||
else if (baseValueChanged) |
|||
{ |
|||
owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false); |
|||
} |
|||
} |
|||
|
|||
private void SetAndRaiseCore( |
|||
ValueStore owner, |
|||
StyledPropertyBase<T> property, |
|||
T value, |
|||
BindingPriority priority, |
|||
T baseValue, |
|||
BindingPriority basePriority) |
|||
{ |
|||
Debug.Assert(priority < BindingPriority.Inherited); |
|||
Debug.Assert(basePriority > BindingPriority.Animation); |
|||
Debug.Assert(priority <= basePriority); |
|||
|
|||
var oldValue = Value; |
|||
var valueChanged = false; |
|||
var baseValueChanged = false; |
|||
var v = value; |
|||
var bv = baseValue; |
|||
|
|||
if (_uncommon?._coerce is { } coerce) |
|||
{ |
|||
v = coerce(owner.Owner, value); |
|||
bv = coerce(owner.Owner, baseValue); |
|||
} |
|||
|
|||
if (priority != BindingPriority.Unset && !EqualityComparer<T>.Default.Equals(Value, v)) |
|||
{ |
|||
Value = v; |
|||
valueChanged = true; |
|||
if (_uncommon is not null) |
|||
_uncommon._uncoercedValue = value; |
|||
} |
|||
|
|||
if (priority != BindingPriority.Unset && |
|||
(BasePriority == BindingPriority.Unset || |
|||
!EqualityComparer<T>.Default.Equals(_baseValue, bv))) |
|||
{ |
|||
_baseValue = v; |
|||
baseValueChanged = true; |
|||
if (_uncommon is not null) |
|||
_uncommon._uncoercedValue = baseValue; |
|||
} |
|||
|
|||
Priority = priority; |
|||
BasePriority = basePriority; |
|||
|
|||
if (valueChanged) |
|||
{ |
|||
using var notifying = PropertyNotifying.Start(owner.Owner, property); |
|||
owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true); |
|||
if (property.Inherits) |
|||
owner.OnInheritedEffectiveValueChanged(property, oldValue, this); |
|||
} |
|||
|
|||
if (baseValueChanged) |
|||
{ |
|||
owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false); |
|||
} |
|||
} |
|||
|
|||
private class UncommonFields |
|||
{ |
|||
public Func<IAvaloniaObject, T, T>? _coerce; |
|||
public T? _uncoercedValue; |
|||
public T? _uncoercedBaseValue; |
|||
} |
|||
} |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal interface IBatchUpdate |
|||
{ |
|||
void BeginBatchUpdate(); |
|||
void EndBatchUpdate(); |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an untyped interface to <see cref="IPriorityValueEntry{T}"/>.
|
|||
/// </summary>
|
|||
internal interface IPriorityValueEntry : IValue |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an object that can act as an entry in a <see cref="PriorityValue{T}"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T> |
|||
{ |
|||
void Reparent(PriorityValue<T> parent); |
|||
} |
|||
} |
|||
@ -1,28 +0,0 @@ |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an untyped interface to <see cref="IValue{T}"/>.
|
|||
/// </summary>
|
|||
internal interface IValue |
|||
{ |
|||
BindingPriority Priority { get; } |
|||
Optional<object?> GetValue(); |
|||
void Start(); |
|||
void RaiseValueChanged( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
Optional<object?> oldValue, |
|||
Optional<object?> newValue); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an object that can act as an entry in a <see cref="ValueStore"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
internal interface IValue<T> : IValue |
|||
{ |
|||
Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an untyped value entry in a <see cref="ValueFrame"/>.
|
|||
/// </summary>
|
|||
internal interface IValueEntry |
|||
{ |
|||
bool HasValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the property that this value applies to.
|
|||
/// </summary>
|
|||
AvaloniaProperty Property { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value associated with the entry.
|
|||
/// </summary>
|
|||
/// <exception cref="AvaloniaInternalException">
|
|||
/// The entry has no value.
|
|||
/// </exception>
|
|||
object? GetValue(); |
|||
|
|||
/// <summary>
|
|||
/// Called when the value entry is removed from the value store.
|
|||
/// </summary>
|
|||
void Unsubscribe(); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a typed value entry in a <see cref="ValueFrame"/>.
|
|||
/// </summary>
|
|||
internal interface IValueEntry<T> : IValueEntry |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the value associated with the entry.
|
|||
/// </summary>
|
|||
/// <exception cref="AvaloniaInternalException">
|
|||
/// The entry has no value.
|
|||
/// </exception>
|
|||
new T GetValue(); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class ImmediateValueEntry<T> : IValueEntry<T>, IDisposable |
|||
{ |
|||
private readonly ImmediateValueFrame _owner; |
|||
private readonly T _value; |
|||
|
|||
public ImmediateValueEntry( |
|||
ImmediateValueFrame owner, |
|||
StyledPropertyBase<T> property, |
|||
T value) |
|||
{ |
|||
_owner = owner; |
|||
_value = value; |
|||
Property = property; |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
public bool HasValue => true; |
|||
AvaloniaProperty IValueEntry.Property => Property; |
|||
|
|||
public void Unsubscribe() { } |
|||
|
|||
public void Dispose() => _owner.OnEntryDisposed(this); |
|||
|
|||
object? IValueEntry.GetValue() => _value; |
|||
T IValueEntry<T>.GetValue() => _value; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Holds values in a <see cref="ValueStore"/> set by one of the SetValue or AddBinding
|
|||
/// overloads with non-LocalValue priority.
|
|||
/// </summary>
|
|||
internal class ImmediateValueFrame : ValueFrame |
|||
{ |
|||
public ImmediateValueFrame(BindingPriority priority) |
|||
{ |
|||
Priority = priority; |
|||
} |
|||
|
|||
public TypedBindingEntry<T> AddBinding<T>( |
|||
StyledPropertyBase<T> property, |
|||
IObservable<BindingValue<T>> source) |
|||
{ |
|||
var e = new TypedBindingEntry<T>(this, property, source); |
|||
Add(e); |
|||
return e; |
|||
} |
|||
|
|||
public TypedBindingEntry<T> AddBinding<T>( |
|||
StyledPropertyBase<T> property, |
|||
IObservable<T> source) |
|||
{ |
|||
var e = new TypedBindingEntry<T>(this, property, source); |
|||
Add(e); |
|||
return e; |
|||
} |
|||
|
|||
public SourceUntypedBindingEntry<T> AddBinding<T>( |
|||
StyledPropertyBase<T> property, |
|||
IObservable<object?> source) |
|||
{ |
|||
var e = new SourceUntypedBindingEntry<T>(this, property, source); |
|||
Add(e); |
|||
return e; |
|||
} |
|||
|
|||
public ImmediateValueEntry<T> AddValue<T>(StyledPropertyBase<T> property, T value) |
|||
{ |
|||
var e = new ImmediateValueEntry<T>(this, property, value); |
|||
Add(e); |
|||
return e; |
|||
} |
|||
|
|||
public void OnEntryDisposed(IValueEntry value) |
|||
{ |
|||
Remove(value.Property); |
|||
Owner?.OnValueEntryRemoved(this, value.Property); |
|||
} |
|||
|
|||
protected override bool GetIsActive(out bool hasChanged) |
|||
{ |
|||
hasChanged = false; |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class LocalValueBindingObserver<T> : IObserver<T>, |
|||
IObserver<BindingValue<T>>, |
|||
IDisposable |
|||
{ |
|||
private readonly ValueStore _owner; |
|||
private IDisposable? _subscription; |
|||
|
|||
public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase<T> property) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
} |
|||
|
|||
public StyledPropertyBase<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 OnNext(T value) |
|||
{ |
|||
if (Property.ValidateValue?.Invoke(value) != false) |
|||
_owner.SetValue(Property, value, BindingPriority.LocalValue); |
|||
else |
|||
_owner.ClearLocalValue(Property); |
|||
} |
|||
|
|||
public void OnNext(BindingValue<T> value) |
|||
{ |
|||
LoggingUtils.LogIfNecessary(_owner.Owner, Property, value); |
|||
|
|||
if (value.HasValue) |
|||
_owner.SetValue(Property, value.Value, BindingPriority.LocalValue); |
|||
else if (value.Type != BindingValueType.DataValidationError) |
|||
_owner.ClearLocalValue(Property); |
|||
} |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Stores a value with local value priority in a <see cref="ValueStore"/> or
|
|||
/// <see cref="PriorityValue{T}"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
internal class LocalValueEntry<T> : IValue<T> |
|||
{ |
|||
private T _value; |
|||
|
|||
public LocalValueEntry(T value) => _value = value; |
|||
public BindingPriority Priority => BindingPriority.LocalValue; |
|||
Optional<object?> IValue.GetValue() => new Optional<object?>(_value); |
|||
|
|||
public Optional<T> GetValue(BindingPriority maxPriority) |
|||
{ |
|||
return BindingPriority.LocalValue >= maxPriority ? _value : Optional<T>.Empty; |
|||
} |
|||
|
|||
public void SetValue(T value) => _value = value; |
|||
public void Start() { } |
|||
|
|||
public void RaiseValueChanged( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
Optional<object?> oldValue, |
|||
Optional<object?> newValue) |
|||
{ |
|||
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( |
|||
owner, |
|||
(AvaloniaProperty<T>)property, |
|||
oldValue.Cast<T>(), |
|||
newValue.Cast<T>(), |
|||
BindingPriority.LocalValue)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class LocalValueUntypedBindingObserver<T> : IObserver<object?>, |
|||
IDisposable |
|||
{ |
|||
private readonly ValueStore _owner; |
|||
private IDisposable? _subscription; |
|||
|
|||
public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase<T> property) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
|
|||
public void Start(IObservable<object?> 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 OnNext(object? value) |
|||
{ |
|||
if (value is BindingNotification n) |
|||
{ |
|||
value = n.Value; |
|||
LoggingUtils.LogIfNecessary(_owner.Owner, Property, n); |
|||
} |
|||
|
|||
if (value == AvaloniaProperty.UnsetValue) |
|||
{ |
|||
_owner.ClearLocalValue(Property); |
|||
} |
|||
else if (value == BindingOperations.DoNothing) |
|||
{ |
|||
// Do nothing!
|
|||
} |
|||
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) |
|||
{ |
|||
_owner.SetValue(Property, typedValue, BindingPriority.LocalValue); |
|||
} |
|||
else |
|||
{ |
|||
_owner.ClearLocalValue(Property); |
|||
LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal static class LoggingUtils |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void LogIfNecessary( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
BindingNotification value) |
|||
{ |
|||
if (value.ErrorType != BindingErrorType.None) |
|||
Log(owner, property, value.Error!); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void LogIfNecessary<T>( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
BindingValue<T> value) |
|||
{ |
|||
if (value.HasError) |
|||
Log(owner, property, value.Error!); |
|||
} |
|||
|
|||
public static void LogInvalidValue( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
Type expectedType, |
|||
object? value) |
|||
{ |
|||
if (value is not null) |
|||
{ |
|||
owner.GetBindingWarningLogger(property, null)?.Log( |
|||
owner, |
|||
"Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})", |
|||
owner, |
|||
property, |
|||
expectedType, |
|||
value, |
|||
value.GetType()); |
|||
} |
|||
else |
|||
{ |
|||
owner.GetBindingWarningLogger(property, null)?.Log( |
|||
owner, |
|||
"Error in binding to {Target}.{Property}: expected {ExpectedType}, got null", |
|||
owner, |
|||
property, |
|||
expectedType); |
|||
} |
|||
} |
|||
|
|||
private static void Log( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
Exception e) |
|||
{ |
|||
if (e is TargetInvocationException t) |
|||
e = t.InnerException!; |
|||
|
|||
if (e is AggregateException a) |
|||
{ |
|||
foreach (var i in a.InnerExceptions) |
|||
Log(owner, property, i); |
|||
} |
|||
else |
|||
{ |
|||
owner.GetBindingWarningLogger(property, e)?.Log( |
|||
owner, |
|||
"Error in binding to {Target}.{Property}: {Message}", |
|||
owner, |
|||
property, |
|||
e.Message); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,326 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an untyped interface to <see cref="PriorityValue{T}"/>.
|
|||
/// </summary>
|
|||
interface IPriorityValue : IValue |
|||
{ |
|||
void UpdateEffectiveValue(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stores a set of prioritized values and bindings in a <see cref="ValueStore"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
/// <remarks>
|
|||
/// When more than a single value or binding is applied to a property in an
|
|||
/// <see cref="AvaloniaObject"/>, the entry in the <see cref="ValueStore"/> is converted into
|
|||
/// a <see cref="PriorityValue{T}"/>. This class holds any number of
|
|||
/// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
|
|||
/// they were added) plus a local value.
|
|||
/// </remarks>
|
|||
internal class PriorityValue<T> : IPriorityValue, IValue<T>, IBatchUpdate |
|||
{ |
|||
private readonly AvaloniaObject _owner; |
|||
private readonly ValueStore _store; |
|||
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>(); |
|||
private readonly Func<IAvaloniaObject, T, T>? _coerceValue; |
|||
private Optional<T> _localValue; |
|||
private Optional<T> _value; |
|||
private bool _isCalculatingValue; |
|||
private bool _batchUpdate; |
|||
|
|||
public PriorityValue( |
|||
AvaloniaObject owner, |
|||
StyledPropertyBase<T> property, |
|||
ValueStore store) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
_store = store; |
|||
|
|||
if (property.HasCoercion) |
|||
{ |
|||
var metadata = property.GetMetadata(owner.GetType()); |
|||
_coerceValue = metadata.CoerceValue; |
|||
} |
|||
} |
|||
|
|||
public PriorityValue( |
|||
AvaloniaObject owner, |
|||
StyledPropertyBase<T> property, |
|||
ValueStore store, |
|||
IPriorityValueEntry<T> existing) |
|||
: this(owner, property, store) |
|||
{ |
|||
existing.Reparent(this); |
|||
_entries.Add(existing); |
|||
|
|||
if (existing is IBindingEntry binding && |
|||
existing.Priority == BindingPriority.LocalValue) |
|||
{ |
|||
// Bit of a special case here: if we have a local value binding that is being
|
|||
// promoted to a priority value we need to make sure the binding is subscribed
|
|||
// even if we've got a batch operation in progress because otherwise we don't know
|
|||
// whether the binding or a subsequent SetValue with local priority will win. A
|
|||
// notification won't be sent during batch update anyway because it will be
|
|||
// caught and stored for later by the ValueStore.
|
|||
binding.Start(ignoreBatchUpdate: true); |
|||
} |
|||
|
|||
var v = existing.GetValue(); |
|||
|
|||
if (v.HasValue) |
|||
{ |
|||
_value = v; |
|||
Priority = existing.Priority; |
|||
} |
|||
} |
|||
|
|||
public PriorityValue( |
|||
AvaloniaObject owner, |
|||
StyledPropertyBase<T> property, |
|||
ValueStore sink, |
|||
LocalValueEntry<T> existing) |
|||
: this(owner, property, sink) |
|||
{ |
|||
_value = _localValue = existing.GetValue(BindingPriority.LocalValue); |
|||
Priority = BindingPriority.LocalValue; |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
public BindingPriority Priority { get; private set; } = BindingPriority.Unset; |
|||
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries; |
|||
Optional<object?> IValue.GetValue() => _value.ToObject(); |
|||
|
|||
public void BeginBatchUpdate() |
|||
{ |
|||
_batchUpdate = true; |
|||
|
|||
foreach (var entry in _entries) |
|||
{ |
|||
(entry as IBatchUpdate)?.BeginBatchUpdate(); |
|||
} |
|||
} |
|||
|
|||
public void EndBatchUpdate() |
|||
{ |
|||
_batchUpdate = false; |
|||
|
|||
foreach (var entry in _entries) |
|||
{ |
|||
(entry as IBatchUpdate)?.EndBatchUpdate(); |
|||
} |
|||
|
|||
UpdateEffectiveValue(null); |
|||
} |
|||
|
|||
public void ClearLocalValue() |
|||
{ |
|||
_localValue = default; |
|||
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>( |
|||
_owner, |
|||
Property, |
|||
default, |
|||
default, |
|||
BindingPriority.LocalValue)); |
|||
} |
|||
|
|||
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation) |
|||
{ |
|||
if (Priority == BindingPriority.Unset) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
if (Priority >= maxPriority) |
|||
{ |
|||
return _value; |
|||
} |
|||
|
|||
return CalculateValue(maxPriority).Item1; |
|||
} |
|||
|
|||
public IDisposable? SetValue(T value, BindingPriority priority) |
|||
{ |
|||
IDisposable? result = null; |
|||
|
|||
if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
_localValue = value; |
|||
} |
|||
else |
|||
{ |
|||
var insert = FindInsertPoint(priority); |
|||
var entry = new ConstantValueEntry<T>(Property, value, priority, new ValueOwner<T>(this)); |
|||
_entries.Insert(insert, entry); |
|||
result = entry; |
|||
} |
|||
|
|||
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>( |
|||
_owner, |
|||
Property, |
|||
default, |
|||
value, |
|||
priority)); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority) |
|||
{ |
|||
var binding = new BindingEntry<T>(_owner, Property, source, priority, new(this)); |
|||
var insert = FindInsertPoint(binding.Priority); |
|||
_entries.Insert(insert, binding); |
|||
|
|||
if (_batchUpdate) |
|||
{ |
|||
binding.BeginBatchUpdate(); |
|||
|
|||
if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
binding.Start(ignoreBatchUpdate: true); |
|||
} |
|||
} |
|||
|
|||
return binding; |
|||
} |
|||
|
|||
public void UpdateEffectiveValue() => UpdateEffectiveValue(null); |
|||
public void Start() => UpdateEffectiveValue(null); |
|||
|
|||
public void RaiseValueChanged( |
|||
AvaloniaObject owner, |
|||
AvaloniaProperty property, |
|||
Optional<object?> oldValue, |
|||
Optional<object?> newValue) |
|||
{ |
|||
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( |
|||
owner, |
|||
(AvaloniaProperty<T>)property, |
|||
oldValue.Cast<T>(), |
|||
newValue.Cast<T>(), |
|||
Priority)); |
|||
} |
|||
|
|||
public void ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change) |
|||
{ |
|||
if (change.Priority == BindingPriority.LocalValue) |
|||
{ |
|||
_localValue = default; |
|||
} |
|||
|
|||
if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c) |
|||
{ |
|||
UpdateEffectiveValue(c); |
|||
} |
|||
} |
|||
|
|||
public void Completed(IPriorityValueEntry entry, Optional<T> oldValue) |
|||
{ |
|||
_entries.Remove((IPriorityValueEntry<T>)entry); |
|||
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>( |
|||
_owner, |
|||
Property, |
|||
oldValue, |
|||
default, |
|||
entry.Priority)); |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority) |
|||
{ |
|||
_isCalculatingValue = true; |
|||
|
|||
try |
|||
{ |
|||
for (var i = _entries.Count - 1; i >= 0; --i) |
|||
{ |
|||
var entry = _entries[i]; |
|||
|
|||
if (entry.Priority < maxPriority) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
entry.Start(); |
|||
|
|||
if (entry.Priority >= BindingPriority.LocalValue && |
|||
maxPriority <= BindingPriority.LocalValue && |
|||
_localValue.HasValue) |
|||
{ |
|||
return (_localValue, BindingPriority.LocalValue); |
|||
} |
|||
|
|||
var entryValue = entry.GetValue(); |
|||
|
|||
if (entryValue.HasValue) |
|||
{ |
|||
return (entryValue, entry.Priority); |
|||
} |
|||
} |
|||
|
|||
if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue) |
|||
{ |
|||
return (_localValue, BindingPriority.LocalValue); |
|||
} |
|||
|
|||
return (default, BindingPriority.Unset); |
|||
} |
|||
finally |
|||
{ |
|||
_isCalculatingValue = false; |
|||
} |
|||
} |
|||
|
|||
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change) |
|||
{ |
|||
var (value, priority) = CalculateValue(BindingPriority.Animation); |
|||
|
|||
if (value.HasValue && _coerceValue != null) |
|||
{ |
|||
value = _coerceValue(_owner, value.Value); |
|||
} |
|||
|
|||
Priority = priority; |
|||
|
|||
if (value != _value) |
|||
{ |
|||
var old = _value; |
|||
_value = value; |
|||
|
|||
_store.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( |
|||
_owner, |
|||
Property, |
|||
old, |
|||
value, |
|||
Priority)); |
|||
} |
|||
else if (change is object) |
|||
{ |
|||
change.MarkNonEffectiveValue(); |
|||
change.SetOldValue(default); |
|||
_store.ValueChanged(change); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Raises <see cref="AvaloniaProperty.Notifying"/> where necessary.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Uses the disposable pattern to ensure that the closing Notifying call is made even in the
|
|||
/// presence of exceptions.
|
|||
/// </remarks>
|
|||
internal readonly struct PropertyNotifying : IDisposable |
|||
{ |
|||
private readonly AvaloniaObject _owner; |
|||
private readonly AvaloniaProperty _property; |
|||
|
|||
private PropertyNotifying(AvaloniaObject owner, AvaloniaProperty property) |
|||
{ |
|||
Debug.Assert(property.Notifying is not null); |
|||
_owner = owner; |
|||
_property = property; |
|||
_property.Notifying!(owner, true); |
|||
} |
|||
|
|||
public void Dispose() => _property.Notifying!(_owner, false); |
|||
|
|||
public static PropertyNotifying? Start(AvaloniaObject owner, AvaloniaProperty property) |
|||
{ |
|||
if (property.Notifying is null) |
|||
return null; |
|||
return new PropertyNotifying(owner, property); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
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?> |
|||
{ |
|||
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> |
|||
{ |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
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 untyped.
|
|||
/// </summary>
|
|||
internal class UntypedBindingEntry : BindingEntryBase<object?, object?> |
|||
{ |
|||
private readonly Func<object?, bool>? _validate; |
|||
|
|||
public UntypedBindingEntry( |
|||
ValueFrame frame, |
|||
AvaloniaProperty property, |
|||
IObservable<object?> source) |
|||
: base(frame, property, source) |
|||
{ |
|||
_validate = ((IStyledPropertyAccessor)property).ValidateValue; |
|||
} |
|||
|
|||
protected override BindingValue<object?> ConvertAndValidate(object? value) |
|||
{ |
|||
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate); |
|||
} |
|||
|
|||
protected override BindingValue<object?> ConvertAndValidate(BindingValue<object?> value) |
|||
{ |
|||
throw new NotSupportedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal static class UntypedValueUtils |
|||
{ |
|||
public static BindingValue<T> ConvertAndValidate<T>( |
|||
object? value, |
|||
Type targetType, |
|||
Func<T, bool>? validate) |
|||
{ |
|||
var v = BindingValue<T>.FromUntyped(value, targetType); |
|||
|
|||
if (v.HasValue && validate?.Invoke(v.Value) == false) |
|||
{ |
|||
return BindingValue<T>.BindingError( |
|||
new InvalidCastException($"'{v.Value}' is not a valid value.")); |
|||
} |
|||
|
|||
return v; |
|||
} |
|||
|
|||
public static bool TryConvertAndValidate( |
|||
AvaloniaProperty property, |
|||
object? value, |
|||
out object? result) |
|||
{ |
|||
if (TypeUtilities.TryConvertImplicit(property.PropertyType, value, out result)) |
|||
return ((IStyledPropertyAccessor)property).ValidateValue(result); |
|||
|
|||
result = default; |
|||
return false; |
|||
} |
|||
|
|||
public static bool TryConvertAndValidate<T>( |
|||
StyledPropertyBase<T> property, |
|||
object? value, |
|||
[MaybeNullWhen(false)] out T result) |
|||
{ |
|||
if (TypeUtilities.TryConvertImplicit(typeof(T), value, out var v)) |
|||
{ |
|||
result = (T)v!; |
|||
|
|||
if (property.ValidateValue?.Invoke(result) != false) |
|||
return true; |
|||
} |
|||
|
|||
result = default; |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
using Avalonia.Utilities; |
|||
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal abstract class ValueFrame |
|||
{ |
|||
private List<IValueEntry>? _entries; |
|||
private AvaloniaPropertyDictionary<IValueEntry> _index; |
|||
private ValueStore? _owner; |
|||
private bool _isShared; |
|||
|
|||
public int EntryCount => _index.Count; |
|||
public bool IsActive => GetIsActive(out _); |
|||
public ValueStore? Owner => !_isShared ? _owner : |
|||
throw new AvaloniaInternalException("Cannot get owner for shared ValueFrame"); |
|||
public BindingPriority Priority { get; protected set; } |
|||
|
|||
public bool Contains(AvaloniaProperty property) => _index.ContainsKey(property); |
|||
|
|||
public IValueEntry GetEntry(int index) => _entries?[index] ?? _index[0]; |
|||
|
|||
public void SetOwner(ValueStore? owner) |
|||
{ |
|||
if (_owner is not null && owner is not null) |
|||
throw new AvaloniaInternalException("ValueFrame already has an owner."); |
|||
if (!_isShared) |
|||
_owner = owner; |
|||
} |
|||
|
|||
public bool TryGetEntryIfActive( |
|||
AvaloniaProperty property, |
|||
[NotNullWhen(true)] out IValueEntry? entry, |
|||
out bool activeChanged) |
|||
{ |
|||
if (_index.TryGetValue(property, out entry)) |
|||
return GetIsActive(out activeChanged); |
|||
activeChanged = false; |
|||
return false; |
|||
} |
|||
|
|||
public void OnBindingCompleted(IValueEntry binding) |
|||
{ |
|||
var property = binding.Property; |
|||
Remove(property); |
|||
Owner?.OnValueEntryRemoved(this, property); |
|||
} |
|||
|
|||
public virtual void Dispose() |
|||
{ |
|||
for (var i = 0; i < _index.Count; ++i) |
|||
_index[i].Unsubscribe(); |
|||
} |
|||
|
|||
protected abstract bool GetIsActive(out bool hasChanged); |
|||
|
|||
protected void MakeShared() |
|||
{ |
|||
_isShared = true; |
|||
_owner = null; |
|||
} |
|||
|
|||
protected void Add(IValueEntry value) |
|||
{ |
|||
Debug.Assert(!value.Property.IsDirect); |
|||
|
|||
if (_entries is null && _index.Count == 1) |
|||
{ |
|||
_entries = new(); |
|||
_entries.Add(_index[0]); |
|||
} |
|||
|
|||
_index.Add(value.Property, value); |
|||
_entries?.Add(value); |
|||
} |
|||
|
|||
protected void Remove(AvaloniaProperty property) |
|||
{ |
|||
Debug.Assert(!property.IsDirect); |
|||
|
|||
if (_entries is not null) |
|||
{ |
|||
var count = _entries.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
if (_entries[i].Property == property) |
|||
{ |
|||
_entries.RemoveAt(i); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
_index.Remove(property); |
|||
} |
|||
} |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a union type of <see cref="ValueStore"/> and <see cref="PriorityValue{T}"/>,
|
|||
/// which are the valid owners of a value store <see cref="IValue"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
internal readonly struct ValueOwner<T> |
|||
{ |
|||
private readonly ValueStore? _store; |
|||
private readonly PriorityValue<T>? _priorityValue; |
|||
|
|||
public ValueOwner(ValueStore o) |
|||
{ |
|||
_store = o; |
|||
_priorityValue = null; |
|||
} |
|||
|
|||
public ValueOwner(PriorityValue<T> v) |
|||
{ |
|||
_store = null; |
|||
_priorityValue = v; |
|||
} |
|||
|
|||
public bool IsValueStore => _store is not null; |
|||
|
|||
public void Completed(StyledPropertyBase<T> property, IPriorityValueEntry entry, Optional<T> oldValue) |
|||
{ |
|||
if (_store is not null) |
|||
_store?.Completed(property, entry, oldValue); |
|||
else |
|||
_priorityValue!.Completed(entry, oldValue); |
|||
} |
|||
|
|||
public void ValueChanged(AvaloniaPropertyChangedEventArgs<T> e) |
|||
{ |
|||
if (_store is not null) |
|||
_store?.ValueChanged(e); |
|||
else |
|||
_priorityValue!.ValueChanged(e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,962 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
using Avalonia.Diagnostics; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class ValueStore |
|||
{ |
|||
private readonly List<ValueFrame> _frames = new(); |
|||
private Dictionary<int, IDisposable>? _localValueBindings; |
|||
private AvaloniaPropertyDictionary<EffectiveValue> _effectiveValues; |
|||
private int _inheritedValueCount; |
|||
private int _isEvaluating; |
|||
private int _frameGeneration; |
|||
private int _styling; |
|||
|
|||
public ValueStore(AvaloniaObject owner) => Owner = owner; |
|||
|
|||
public AvaloniaObject Owner { get; } |
|||
public ValueStore? InheritanceAncestor { get; private set; } |
|||
public bool IsEvaluating => _isEvaluating > 0; |
|||
public IReadOnlyList<ValueFrame> Frames => _frames; |
|||
|
|||
public void BeginStyling() => ++_styling; |
|||
|
|||
public void EndStyling() |
|||
{ |
|||
if (--_styling == 0) |
|||
ReevaluateEffectiveValues(); |
|||
} |
|||
|
|||
public void AddFrame(ValueFrame style) |
|||
{ |
|||
InsertFrame(style); |
|||
ReevaluateEffectiveValues(); |
|||
} |
|||
|
|||
public IDisposable AddBinding<T>( |
|||
StyledPropertyBase<T> property, |
|||
IObservable<BindingValue<T>> source, |
|||
BindingPriority priority) |
|||
{ |
|||
if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
var observer = new LocalValueBindingObserver<T>(this, property); |
|||
DisposeExistingLocalValueBinding(property); |
|||
_localValueBindings ??= new(); |
|||
_localValueBindings[property.Id] = observer; |
|||
observer.Start(source); |
|||
return observer; |
|||
} |
|||
else |
|||
{ |
|||
var effective = GetEffectiveValue(property); |
|||
|
|||
var frame = GetOrCreateImmediateValueFrame(property, priority, out _); |
|||
var result = frame.AddBinding(property, source); |
|||
|
|||
if (effective is null || priority <= effective.Priority) |
|||
result.Start(); |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
|
|||
public IDisposable AddBinding<T>( |
|||
StyledPropertyBase<T> property, |
|||
IObservable<T> source, |
|||
BindingPriority priority) |
|||
{ |
|||
if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
var observer = new LocalValueBindingObserver<T>(this, property); |
|||
DisposeExistingLocalValueBinding(property); |
|||
_localValueBindings ??= new(); |
|||
_localValueBindings[property.Id] = observer; |
|||
observer.Start(source); |
|||
return observer; |
|||
} |
|||
else |
|||
{ |
|||
var effective = GetEffectiveValue(property); |
|||
|
|||
var frame = GetOrCreateImmediateValueFrame(property, priority, out _); |
|||
var result = frame.AddBinding(property, source); |
|||
|
|||
if (effective is null || priority <= effective.Priority) |
|||
result.Start(); |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
|
|||
public IDisposable AddBinding<T>( |
|||
StyledPropertyBase<T> property, |
|||
IObservable<object?> source, |
|||
BindingPriority priority) |
|||
{ |
|||
if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
var observer = new LocalValueUntypedBindingObserver<T>(this, property); |
|||
DisposeExistingLocalValueBinding(property); |
|||
_localValueBindings ??= new(); |
|||
_localValueBindings[property.Id] = observer; |
|||
observer.Start(source); |
|||
return observer; |
|||
} |
|||
else |
|||
{ |
|||
var effective = GetEffectiveValue(property); |
|||
|
|||
var frame = GetOrCreateImmediateValueFrame(property, priority, out _); |
|||
var result = frame.AddBinding(property, source); |
|||
|
|||
if (effective is null || priority <= effective.Priority) |
|||
result.Start(); |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
|
|||
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<BindingValue<T>> source) |
|||
{ |
|||
var observer = new DirectBindingObserver<T>(this, property); |
|||
DisposeExistingLocalValueBinding(property); |
|||
_localValueBindings ??= new(); |
|||
_localValueBindings[property.Id] = observer; |
|||
observer.Start(source); |
|||
return observer; |
|||
} |
|||
|
|||
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<T> source) |
|||
{ |
|||
var observer = new DirectBindingObserver<T>(this, property); |
|||
DisposeExistingLocalValueBinding(property); |
|||
_localValueBindings ??= new(); |
|||
_localValueBindings[property.Id] = observer; |
|||
observer.Start(source); |
|||
return observer; |
|||
} |
|||
|
|||
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<object?> source) |
|||
{ |
|||
var observer = new DirectUntypedBindingObserver<T>(this, property); |
|||
DisposeExistingLocalValueBinding(property); |
|||
_localValueBindings ??= new(); |
|||
_localValueBindings[property.Id] = observer; |
|||
observer.Start(source); |
|||
return observer; |
|||
} |
|||
|
|||
public void ClearLocalValue(AvaloniaProperty property) |
|||
{ |
|||
if (TryGetEffectiveValue(property, out var effective) && |
|||
effective.Priority == BindingPriority.LocalValue) |
|||
{ |
|||
ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true); |
|||
} |
|||
} |
|||
|
|||
public IDisposable? SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority) |
|||
{ |
|||
if (property.ValidateValue?.Invoke(value) == false) |
|||
{ |
|||
throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); |
|||
} |
|||
|
|||
if (priority != BindingPriority.LocalValue) |
|||
{ |
|||
var frame = GetOrCreateImmediateValueFrame(property, priority, out _); |
|||
var result = frame.AddValue(property, value); |
|||
|
|||
if (TryGetEffectiveValue(property, out var existing)) |
|||
{ |
|||
var effective = (EffectiveValue<T>)existing; |
|||
effective.SetAndRaise(this, result, priority); |
|||
} |
|||
else |
|||
{ |
|||
var effectiveValue = new EffectiveValue<T>(Owner, property); |
|||
AddEffectiveValue(property, effectiveValue); |
|||
effectiveValue.SetAndRaise(this, result, priority); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
else |
|||
{ |
|||
if (TryGetEffectiveValue(property, out var existing)) |
|||
{ |
|||
var effective = (EffectiveValue<T>)existing; |
|||
effective.SetLocalValueAndRaise(this, property, value); |
|||
} |
|||
else |
|||
{ |
|||
var effectiveValue = new EffectiveValue<T>(Owner, property); |
|||
AddEffectiveValue(property, effectiveValue); |
|||
effectiveValue.SetLocalValueAndRaise(this, property, value); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public object? GetValue(AvaloniaProperty property) |
|||
{ |
|||
if (_effectiveValues.TryGetValue(property, out var v)) |
|||
return v.Value; |
|||
if (property.Inherits && TryGetInheritedValue(property, out v)) |
|||
return v.Value; |
|||
|
|||
return GetDefaultValue(property); |
|||
} |
|||
|
|||
public T GetValue<T>(StyledPropertyBase<T> property) |
|||
{ |
|||
if (_effectiveValues.TryGetValue(property, out var v)) |
|||
return ((EffectiveValue<T>)v).Value; |
|||
if (property.Inherits && TryGetInheritedValue(property, out v)) |
|||
return ((EffectiveValue<T>)v).Value; |
|||
return property.GetDefaultValue(Owner.GetType()); |
|||
} |
|||
|
|||
public bool IsAnimating(AvaloniaProperty property) |
|||
{ |
|||
if (_effectiveValues.TryGetValue(property, out var v)) |
|||
return v.Priority <= BindingPriority.Animation; |
|||
return false; |
|||
} |
|||
|
|||
public bool IsSet(AvaloniaProperty property) |
|||
{ |
|||
if (_effectiveValues.TryGetValue(property, out var v)) |
|||
return v.Priority < BindingPriority.Inherited; |
|||
return false; |
|||
} |
|||
|
|||
public void CoerceValue(AvaloniaProperty property) |
|||
{ |
|||
if (_effectiveValues.TryGetValue(property, out var v)) |
|||
v.CoerceValue(this, property); |
|||
} |
|||
|
|||
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property) |
|||
{ |
|||
if (TryGetEffectiveValue(property, out var v) && |
|||
((EffectiveValue<T>)v).TryGetBaseValue(out var baseValue)) |
|||
{ |
|||
return baseValue; |
|||
} |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public bool TryGetInheritedValue( |
|||
AvaloniaProperty property, |
|||
[NotNullWhen(true)] out EffectiveValue? result) |
|||
{ |
|||
Debug.Assert(property.Inherits); |
|||
|
|||
var i = InheritanceAncestor; |
|||
|
|||
while (i is not null) |
|||
{ |
|||
if (i.TryGetEffectiveValue(property, out result)) |
|||
return true; |
|||
i = i.InheritanceAncestor; |
|||
} |
|||
|
|||
result = null; |
|||
return false; |
|||
} |
|||
|
|||
public void SetInheritanceParent(AvaloniaObject? newParent) |
|||
{ |
|||
var values = AvaloniaPropertyDictionaryPool<OldNewValue>.Get(); |
|||
var oldAncestor = InheritanceAncestor; |
|||
var newAncestor = newParent?.GetValueStore(); |
|||
|
|||
if (newAncestor?._inheritedValueCount == 0) |
|||
newAncestor = newAncestor.InheritanceAncestor; |
|||
|
|||
// The old and new inheritance ancestors are the same, nothing to do here.
|
|||
if (oldAncestor == newAncestor) |
|||
return; |
|||
|
|||
// First get the old values from the old inheritance ancestor.
|
|||
var f = oldAncestor; |
|||
|
|||
while (f is not null) |
|||
{ |
|||
var count = f._effectiveValues.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
f._effectiveValues.GetKeyValue(i, out var key, out var value); |
|||
if (key.Inherits) |
|||
values.TryAdd(key, new(value)); |
|||
} |
|||
|
|||
f = f.InheritanceAncestor; |
|||
} |
|||
|
|||
f = newAncestor; |
|||
|
|||
// Get the new values from the new inheritance ancestor.
|
|||
while (f is not null) |
|||
{ |
|||
var count = f._effectiveValues.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
f._effectiveValues.GetKeyValue(i, out var key, out var value); |
|||
|
|||
if (!key.Inherits) |
|||
continue; |
|||
|
|||
if (values.TryGetValue(key, out var existing)) |
|||
{ |
|||
if (existing.NewValue is null) |
|||
values[key] = existing.WithNewValue(value); |
|||
} |
|||
else |
|||
{ |
|||
values.Add(key, new(null, value)); |
|||
} |
|||
} |
|||
|
|||
f = f.InheritanceAncestor; |
|||
} |
|||
|
|||
OnInheritanceAncestorChanged(newAncestor); |
|||
|
|||
// Raise PropertyChanged events where necessary on this object and inheritance children.
|
|||
{ |
|||
var count = values.Count; |
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
values.GetKeyValue(i, out var key, out var v); |
|||
var oldValue = v.OldValue; |
|||
var newValue = v.NewValue; |
|||
|
|||
if (oldValue != newValue) |
|||
InheritedValueChanged(key, oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
AvaloniaPropertyDictionaryPool<OldNewValue>.Release(values); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
|
|||
/// binding produces a new value.
|
|||
/// </summary>
|
|||
/// <param name="entry">The binding entry.</param>
|
|||
/// <param name="priority">The priority of binding which produced a new value.</param>
|
|||
public void OnBindingValueChanged( |
|||
IValueEntry entry, |
|||
BindingPriority priority) |
|||
{ |
|||
Debug.Assert(priority != BindingPriority.LocalValue); |
|||
|
|||
var property = entry.Property; |
|||
|
|||
if (TryGetEffectiveValue(property, out var existing)) |
|||
{ |
|||
if (priority <= existing.BasePriority) |
|||
ReevaluateEffectiveValue(property, existing); |
|||
} |
|||
else |
|||
{ |
|||
AddEffectiveValueAndRaise(property, entry, priority); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
|
|||
/// binding produces an unset value.
|
|||
/// </summary>
|
|||
/// <param name="property">The bound property.</param>
|
|||
/// <param name="priority">The priority of binding which produced a new value.</param>
|
|||
public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority) |
|||
{ |
|||
Debug.Assert(priority != BindingPriority.LocalValue); |
|||
|
|||
if (TryGetEffectiveValue(property, out var existing)) |
|||
{ |
|||
if (priority <= existing.Priority) |
|||
ReevaluateEffectiveValue(property, existing); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by a <see cref="ValueFrame"/> when its <see cref="ValueFrame.IsActive"/>
|
|||
/// state changes.
|
|||
/// </summary>
|
|||
/// <param name="frame">The frame which produced the change.</param>
|
|||
public void OnFrameActivationChanged(ValueFrame frame) |
|||
{ |
|||
if (frame.EntryCount == 0) |
|||
return; |
|||
else if (frame.EntryCount == 1) |
|||
{ |
|||
var property = frame.GetEntry(0).Property; |
|||
_effectiveValues.TryGetValue(property, out var current); |
|||
ReevaluateEffectiveValue(property, current); |
|||
} |
|||
else |
|||
ReevaluateEffectiveValues(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by the parent value store when its inheritance ancestor changes.
|
|||
/// </summary>
|
|||
/// <param name="ancestor">The new inheritance ancestor.</param>
|
|||
public void OnInheritanceAncestorChanged(ValueStore? ancestor) |
|||
{ |
|||
if (ancestor != this) |
|||
{ |
|||
InheritanceAncestor = ancestor; |
|||
if (_inheritedValueCount > 0) |
|||
return; |
|||
} |
|||
|
|||
var children = Owner.GetInheritanceChildren(); |
|||
|
|||
if (children is null) |
|||
return; |
|||
|
|||
var count = children.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
children[i].GetValueStore().OnInheritanceAncestorChanged(ancestor); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
|
|||
/// changes its value on this value store.
|
|||
/// </summary>
|
|||
/// <param name="property">The property whose value changed.</param>
|
|||
/// <param name="oldValue">The old value of the property.</param>
|
|||
/// <param name="value">The effective value instance.</param>
|
|||
public void OnInheritedEffectiveValueChanged<T>( |
|||
StyledPropertyBase<T> property, |
|||
T oldValue, |
|||
EffectiveValue<T> value) |
|||
{ |
|||
Debug.Assert(property.Inherits); |
|||
|
|||
var children = Owner.GetInheritanceChildren(); |
|||
|
|||
if (children is null) |
|||
return; |
|||
|
|||
var count = children.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, value.Value); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
|
|||
/// is removed from the effective values.
|
|||
/// </summary>
|
|||
/// <param name="property">The property whose value changed.</param>
|
|||
/// <param name="oldValue">The old value of the property.</param>
|
|||
public void OnInheritedEffectiveValueDisposed<T>(StyledPropertyBase<T> property, T oldValue) |
|||
{ |
|||
Debug.Assert(property.Inherits); |
|||
|
|||
var children = Owner.GetInheritanceChildren(); |
|||
|
|||
if (children is not null) |
|||
{ |
|||
var defaultValue = property.GetDefaultValue(Owner.GetType()); |
|||
var count = children.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when a <see cref="LocalValueBindingObserver{T}"/> or
|
|||
/// <see cref="DirectBindingObserver{T}"/> completes.
|
|||
/// </summary>
|
|||
/// <param name="property">The previously bound property.</param>
|
|||
/// <param name="observer">The observer.</param>
|
|||
public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer) |
|||
{ |
|||
if (_localValueBindings is not null && |
|||
_localValueBindings.TryGetValue(property.Id, out var existing)) |
|||
{ |
|||
if (existing == observer) |
|||
{ |
|||
_localValueBindings?.Remove(property.Id); |
|||
ClearLocalValue(property); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when an inherited property changes on the value store of the inheritance ancestor.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="oldValue">The old value of the property.</param>
|
|||
/// <param name="newValue">The new value of the property.</param>
|
|||
public void OnAncestorInheritedValueChanged<T>( |
|||
StyledPropertyBase<T> property, |
|||
T oldValue, |
|||
T newValue) |
|||
{ |
|||
Debug.Assert(property.Inherits); |
|||
|
|||
// If the inherited value is set locally, propagation stops here.
|
|||
if (_effectiveValues.ContainsKey(property)) |
|||
return; |
|||
|
|||
using var notifying = PropertyNotifying.Start(Owner, property); |
|||
|
|||
Owner.RaisePropertyChanged( |
|||
property, |
|||
oldValue, |
|||
newValue, |
|||
BindingPriority.Inherited, |
|||
true); |
|||
|
|||
var children = Owner.GetInheritanceChildren(); |
|||
|
|||
if (children is null) |
|||
return; |
|||
|
|||
var count = children.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by a <see cref="ValueFrame"/> to re-evaluate the effective value when a value
|
|||
/// is removed.
|
|||
/// </summary>
|
|||
/// <param name="frame">The frame on which the change occurred.</param>
|
|||
/// <param name="property">The property whose value was removed.</param>
|
|||
public void OnValueEntryRemoved(ValueFrame frame, AvaloniaProperty property) |
|||
{ |
|||
if (frame.EntryCount == 0) |
|||
_frames.Remove(frame); |
|||
|
|||
if (TryGetEffectiveValue(property, out var existing)) |
|||
{ |
|||
if (frame.Priority <= existing.Priority) |
|||
ReevaluateEffectiveValue(property, existing); |
|||
} |
|||
} |
|||
|
|||
public bool RemoveFrame(ValueFrame frame) |
|||
{ |
|||
if (_frames.Remove(frame)) |
|||
{ |
|||
frame.Dispose(); |
|||
++_frameGeneration; |
|||
ReevaluateEffectiveValues(); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property) |
|||
{ |
|||
object? value; |
|||
BindingPriority priority; |
|||
|
|||
if (_effectiveValues.TryGetValue(property, out var v)) |
|||
{ |
|||
value = v.Value; |
|||
priority = v.Priority; |
|||
} |
|||
else if (property.Inherits && TryGetInheritedValue(property, out v)) |
|||
{ |
|||
value = v.Value; |
|||
priority = BindingPriority.Inherited; |
|||
} |
|||
else |
|||
{ |
|||
value = GetDefaultValue(property); |
|||
priority = BindingPriority.Unset; |
|||
} |
|||
|
|||
return new AvaloniaPropertyValue( |
|||
property, |
|||
value, |
|||
priority, |
|||
null); |
|||
} |
|||
|
|||
private int InsertFrame(ValueFrame frame) |
|||
{ |
|||
// Uncomment this line when #8549 is fixed.
|
|||
//Debug.Assert(!_frames.Contains(frame));
|
|||
|
|||
var index = BinarySearchFrame(frame.Priority); |
|||
_frames.Insert(index, frame); |
|||
++_frameGeneration; |
|||
frame.SetOwner(this); |
|||
return index; |
|||
} |
|||
|
|||
private ImmediateValueFrame GetOrCreateImmediateValueFrame( |
|||
AvaloniaProperty property, |
|||
BindingPriority priority, |
|||
out int frameIndex) |
|||
{ |
|||
Debug.Assert(priority != BindingPriority.LocalValue); |
|||
|
|||
var index = BinarySearchFrame(priority); |
|||
|
|||
if (index > 0 && _frames[index - 1] is ImmediateValueFrame f && |
|||
f.Priority == priority && |
|||
!f.Contains(property)) |
|||
{ |
|||
frameIndex = index - 1; |
|||
return f; |
|||
} |
|||
|
|||
var result = new ImmediateValueFrame(priority); |
|||
frameIndex = InsertFrame(result); |
|||
return result; |
|||
} |
|||
|
|||
private void AddEffectiveValue(AvaloniaProperty property, EffectiveValue effectiveValue) |
|||
{ |
|||
_effectiveValues.Add(property, effectiveValue); |
|||
|
|||
if (property.Inherits && _inheritedValueCount++ == 0) |
|||
OnInheritanceAncestorChanged(this); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
|
|||
/// event and notifies inheritance children if necessary .
|
|||
/// </summary>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="entry">The value entry.</param>
|
|||
/// <param name="priority">The value priority.</param>
|
|||
private void AddEffectiveValueAndRaise(AvaloniaProperty property, IValueEntry entry, BindingPriority priority) |
|||
{ |
|||
Debug.Assert(priority < BindingPriority.Inherited); |
|||
var effectiveValue = property.CreateEffectiveValue(Owner); |
|||
AddEffectiveValue(property, effectiveValue); |
|||
effectiveValue.SetAndRaise(this, entry, priority); |
|||
} |
|||
|
|||
private void RemoveEffectiveValue(AvaloniaProperty property, int index) |
|||
{ |
|||
_effectiveValues.RemoveAt(index); |
|||
if (property.Inherits && --_inheritedValueCount == 0) |
|||
OnInheritanceAncestorChanged(InheritanceAncestor); |
|||
} |
|||
|
|||
private bool RemoveEffectiveValue(AvaloniaProperty property) |
|||
{ |
|||
if (_effectiveValues.Remove(property)) |
|||
{ |
|||
if (property.Inherits && --_inheritedValueCount == 0) |
|||
OnInheritanceAncestorChanged(InheritanceAncestor); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private void InheritedValueChanged( |
|||
AvaloniaProperty property, |
|||
EffectiveValue? oldValue, |
|||
EffectiveValue? newValue) |
|||
{ |
|||
Debug.Assert(oldValue != newValue); |
|||
Debug.Assert(oldValue is not null || newValue is not null); |
|||
|
|||
// If the value is set locally, propagaton ends here.
|
|||
if (_effectiveValues.ContainsKey(property) == true) |
|||
return; |
|||
|
|||
using var notifying = PropertyNotifying.Start(Owner, property); |
|||
|
|||
// Raise PropertyChanged on this object if necessary.
|
|||
(oldValue ?? newValue!).RaiseInheritedValueChanged(Owner, property, oldValue, newValue); |
|||
|
|||
var children = Owner.GetInheritanceChildren(); |
|||
|
|||
if (children is null) |
|||
return; |
|||
|
|||
var count = children.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
children[i].GetValueStore().InheritedValueChanged(property, oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
private void ReevaluateEffectiveValue( |
|||
AvaloniaProperty property, |
|||
EffectiveValue? current, |
|||
bool ignoreLocalValue = false) |
|||
{ |
|||
++_isEvaluating; |
|||
|
|||
try |
|||
{ |
|||
restart: |
|||
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
|
|||
// it has finished.
|
|||
if (_styling > 0) |
|||
return; |
|||
|
|||
var generation = _frameGeneration; |
|||
|
|||
// Notify the existing effective value that reevaluation is starting.
|
|||
current?.BeginReevaluation(ignoreLocalValue); |
|||
|
|||
// Iterate the frames to get the effective value.
|
|||
for (var i = _frames.Count - 1; i >= 0; --i) |
|||
{ |
|||
var frame = _frames[i]; |
|||
var priority = frame.Priority; |
|||
var foundEntry = frame.TryGetEntryIfActive(property, out var entry, out var activeChanged); |
|||
|
|||
// If the active state of the frame has changed since the last read, and
|
|||
// the frame holds multiple values then we need to re-evaluate the
|
|||
// effective values of all properties.
|
|||
if (activeChanged && frame.EntryCount > 1) |
|||
{ |
|||
ReevaluateEffectiveValues(); |
|||
return; |
|||
} |
|||
|
|||
// We're interested in the value if:
|
|||
// - There is no current effective value, or
|
|||
// - The value's priority is higher than the current effective value's priority, or
|
|||
// - The value is a non-animation value and its priority is higher than the current
|
|||
// effective value's base priority
|
|||
var isRelevantPriority = current is null || |
|||
priority < current.Priority || |
|||
(priority > BindingPriority.Animation && priority < current.BasePriority); |
|||
|
|||
if (foundEntry && isRelevantPriority && entry!.HasValue) |
|||
{ |
|||
if (current is not null) |
|||
{ |
|||
current.SetAndRaise(this, entry, priority); |
|||
} |
|||
else |
|||
{ |
|||
current = property.CreateEffectiveValue(Owner); |
|||
AddEffectiveValue(property, current); |
|||
current.SetAndRaise(this, entry, priority); |
|||
} |
|||
} |
|||
|
|||
if (generation != _frameGeneration) |
|||
goto restart; |
|||
|
|||
if (current?.Priority < BindingPriority.Unset && |
|||
current?.BasePriority < BindingPriority.Unset) |
|||
break; |
|||
} |
|||
|
|||
current?.EndReevaluation(); |
|||
|
|||
if (current?.Priority == BindingPriority.Unset) |
|||
{ |
|||
if (current.BasePriority == BindingPriority.Unset) |
|||
{ |
|||
RemoveEffectiveValue(property); |
|||
current.DisposeAndRaiseUnset(this, property); |
|||
} |
|||
else |
|||
{ |
|||
current.RemoveAnimationAndRaise(this, property); |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
--_isEvaluating; |
|||
} |
|||
} |
|||
|
|||
private void ReevaluateEffectiveValues() |
|||
{ |
|||
++_isEvaluating; |
|||
|
|||
try |
|||
{ |
|||
restart: |
|||
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
|
|||
// it has finished.
|
|||
if (_styling > 0) |
|||
return; |
|||
|
|||
var generation = _frameGeneration; |
|||
var count = _effectiveValues.Count; |
|||
|
|||
// Notify the existing effective values that reevaluation is starting.
|
|||
for (var i = 0; i < count; ++i) |
|||
_effectiveValues[i].BeginReevaluation(); |
|||
|
|||
// Iterate the frames, setting and creating effective values.
|
|||
for (var i = _frames.Count - 1; i >= 0; --i) |
|||
{ |
|||
var frame = _frames[i]; |
|||
|
|||
if (!frame.IsActive) |
|||
continue; |
|||
|
|||
var priority = frame.Priority; |
|||
|
|||
count = frame.EntryCount; |
|||
|
|||
for (var j = 0; j < count; ++j) |
|||
{ |
|||
var entry = frame.GetEntry(j); |
|||
var property = entry.Property; |
|||
|
|||
// Skip if we already have a value/base value for this property.
|
|||
if (_effectiveValues.TryGetValue(property, out var effectiveValue) && |
|||
effectiveValue.BasePriority < BindingPriority.Unset) |
|||
continue; |
|||
|
|||
if (!entry.HasValue) |
|||
continue; |
|||
|
|||
if (effectiveValue is not null) |
|||
{ |
|||
effectiveValue.SetAndRaise(this, entry, priority); |
|||
} |
|||
else |
|||
{ |
|||
var v = property.CreateEffectiveValue(Owner); |
|||
AddEffectiveValue(property, v); |
|||
v.SetAndRaise(this, entry, priority); |
|||
} |
|||
|
|||
if (generation != _frameGeneration) |
|||
goto restart; |
|||
} |
|||
} |
|||
|
|||
// Remove all effective values that are still unset.
|
|||
for (var i = _effectiveValues.Count - 1; i >= 0; --i) |
|||
{ |
|||
_effectiveValues.GetKeyValue(i, out var key, out var e); |
|||
e.EndReevaluation(); |
|||
|
|||
if (e.Priority == BindingPriority.Unset) |
|||
{ |
|||
RemoveEffectiveValue(key, i); |
|||
e.DisposeAndRaiseUnset(this, key); |
|||
|
|||
if (i > _effectiveValues.Count) |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
--_isEvaluating; |
|||
} |
|||
} |
|||
|
|||
private bool TryGetEffectiveValue( |
|||
AvaloniaProperty property, |
|||
[NotNullWhen(true)] out EffectiveValue? value) |
|||
{ |
|||
if (_effectiveValues.TryGetValue(property, out value)) |
|||
return true; |
|||
value = null; |
|||
return false; |
|||
} |
|||
|
|||
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property) |
|||
{ |
|||
if (_effectiveValues.TryGetValue(property, out var value)) |
|||
return value; |
|||
return null; |
|||
} |
|||
|
|||
private object? GetDefaultValue(AvaloniaProperty property) |
|||
{ |
|||
return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType()); |
|||
} |
|||
|
|||
private void DisposeExistingLocalValueBinding(AvaloniaProperty property) |
|||
{ |
|||
if (_localValueBindings is not null && |
|||
_localValueBindings.TryGetValue(property.Id, out var existing)) |
|||
{ |
|||
existing.Dispose(); |
|||
} |
|||
} |
|||
|
|||
private int BinarySearchFrame(BindingPriority priority) |
|||
{ |
|||
var lo = 0; |
|||
var hi = _frames.Count - 1; |
|||
|
|||
// Binary search insertion point.
|
|||
while (lo <= hi) |
|||
{ |
|||
var i = lo + ((hi - lo) >> 1); |
|||
var order = priority - _frames[i].Priority; |
|||
|
|||
if (order <= 0) |
|||
{ |
|||
lo = i + 1; |
|||
} |
|||
else |
|||
{ |
|||
hi = i - 1; |
|||
} |
|||
} |
|||
|
|||
return lo; |
|||
} |
|||
|
|||
private readonly struct OldNewValue |
|||
{ |
|||
public OldNewValue(EffectiveValue? oldValue) |
|||
{ |
|||
OldValue = oldValue; |
|||
NewValue = null; |
|||
} |
|||
|
|||
public OldNewValue(EffectiveValue? oldValue, EffectiveValue? newValue) |
|||
{ |
|||
OldValue = oldValue; |
|||
NewValue = newValue; |
|||
} |
|||
|
|||
public readonly EffectiveValue? OldValue; |
|||
public readonly EffectiveValue? NewValue; |
|||
|
|||
public OldNewValue WithNewValue(EffectiveValue newValue) => new(OldValue, newValue); |
|||
} |
|||
} |
|||
} |
|||
@ -1,59 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
internal class BindingValueAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
|||
IObserver<T> |
|||
{ |
|||
private readonly IObservable<T> _source; |
|||
private IDisposable? _subscription; |
|||
|
|||
public BindingValueAdapter(IObservable<T> source) => _source = source; |
|||
public void OnCompleted() => PublishCompleted(); |
|||
public void OnError(Exception error) => PublishError(error); |
|||
public void OnNext(T value) => PublishNext(BindingValue<T>.FromUntyped(value)); |
|||
protected override void Subscribed() => _subscription = _source.Subscribe(this); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
} |
|||
|
|||
internal class BindingValueSubjectAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
|||
ISubject<BindingValue<T>> |
|||
{ |
|||
private readonly ISubject<T> _source; |
|||
private readonly Inner _inner; |
|||
private IDisposable? _subscription; |
|||
|
|||
public BindingValueSubjectAdapter(ISubject<T> source) |
|||
{ |
|||
_source = source; |
|||
_inner = new Inner(this); |
|||
} |
|||
|
|||
public void OnCompleted() => _source.OnCompleted(); |
|||
public void OnError(Exception error) => _source.OnError(error); |
|||
|
|||
public void OnNext(BindingValue<T> 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<T> |
|||
{ |
|||
private readonly BindingValueSubjectAdapter<T> _owner; |
|||
|
|||
public Inner(BindingValueSubjectAdapter<T> owner) => _owner = owner; |
|||
|
|||
public void OnCompleted() => _owner.PublishCompleted(); |
|||
public void OnError(Exception error) => _owner.PublishError(error); |
|||
public void OnNext(T value) => _owner.PublishNext(BindingValue<T>.FromUntyped(value)); |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
public static class BindingValueExtensions |
|||
{ |
|||
public static IObservable<BindingValue<T>> ToBindingValue<T>(this IObservable<T> source) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
return new BindingValueAdapter<T>(source); |
|||
} |
|||
|
|||
public static ISubject<BindingValue<T>> ToBindingValue<T>(this ISubject<T> source) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
return new BindingValueSubjectAdapter<T>(source); |
|||
} |
|||
|
|||
public static IObservable<object?> ToUntyped<T>(this IObservable<BindingValue<T>> source) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
return new UntypedBindingAdapter<T>(source); |
|||
} |
|||
|
|||
public static ISubject<object?> ToUntyped<T>(this ISubject<BindingValue<T>> source) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
return new UntypedBindingSubjectAdapter<T>(source); |
|||
} |
|||
} |
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Logging; |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
internal class TypedBindingAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
|||
IObserver<BindingValue<object?>> |
|||
{ |
|||
private readonly IAvaloniaObject _target; |
|||
private readonly AvaloniaProperty<T> _property; |
|||
private readonly IObservable<BindingValue<object?>> _source; |
|||
private IDisposable? _subscription; |
|||
|
|||
public TypedBindingAdapter( |
|||
IAvaloniaObject target, |
|||
AvaloniaProperty<T> property, |
|||
IObservable<BindingValue<object?>> source) |
|||
{ |
|||
_target = target; |
|||
_property = property; |
|||
_source = source; |
|||
} |
|||
|
|||
public void OnNext(BindingValue<object?> value) |
|||
{ |
|||
try |
|||
{ |
|||
PublishNext(value.Convert<T>()); |
|||
} |
|||
catch (InvalidCastException e) |
|||
{ |
|||
var unwrappedValue = value.HasValue ? value.Value : null; |
|||
|
|||
Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log( |
|||
_target, |
|||
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", |
|||
_property.Name, |
|||
_property.PropertyType, |
|||
unwrappedValue, |
|||
unwrappedValue?.GetType()); |
|||
PublishNext(BindingValue<T>.BindingError(e)); |
|||
} |
|||
} |
|||
|
|||
public void OnCompleted() => PublishCompleted(); |
|||
public void OnError(Exception error) => PublishError(error); |
|||
|
|||
public static IObservable<BindingValue<T>> Create( |
|||
IAvaloniaObject target, |
|||
AvaloniaProperty<T> property, |
|||
IObservable<BindingValue<object?>> source) |
|||
{ |
|||
return source is IObservable<BindingValue<T>> result ? |
|||
result : |
|||
new TypedBindingAdapter<T>(target, property, source); |
|||
} |
|||
|
|||
protected override void Subscribed() => _subscription = _source.Subscribe(this); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
} |
|||
} |
|||
@ -1,55 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
internal class UntypedBindingAdapter<T> : SingleSubscriberObservableBase<object?>, |
|||
IObserver<BindingValue<T>> |
|||
{ |
|||
private readonly IObservable<BindingValue<T>> _source; |
|||
private IDisposable? _subscription; |
|||
|
|||
public UntypedBindingAdapter(IObservable<BindingValue<T>> source) => _source = source; |
|||
public void OnCompleted() => PublishCompleted(); |
|||
public void OnError(Exception error) => PublishError(error); |
|||
public void OnNext(BindingValue<T> value) => value.ToUntyped(); |
|||
protected override void Subscribed() => _subscription = _source.Subscribe(this); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
} |
|||
|
|||
internal class UntypedBindingSubjectAdapter<T> : SingleSubscriberObservableBase<object?>, |
|||
ISubject<object?> |
|||
{ |
|||
private readonly ISubject<BindingValue<T>> _source; |
|||
private readonly Inner _inner; |
|||
private IDisposable? _subscription; |
|||
|
|||
public UntypedBindingSubjectAdapter(ISubject<BindingValue<T>> 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<T>.FromUntyped(value)); |
|||
} |
|||
|
|||
protected override void Subscribed() => _subscription = _source.Subscribe(_inner); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
|
|||
private class Inner : IObserver<BindingValue<T>> |
|||
{ |
|||
private readonly UntypedBindingSubjectAdapter<T> _owner; |
|||
|
|||
public Inner(UntypedBindingSubjectAdapter<T> owner) => _owner = owner; |
|||
|
|||
public void OnCompleted() => _owner.PublishCompleted(); |
|||
public void OnError(Exception error) => _owner.PublishError(error); |
|||
public void OnNext(BindingValue<T> value) => _owner.PublishNext(value.ToUntyped()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Avalonia.Styling |
|||
{ |
|||
internal class DirectPropertySetterBindingInstance : ISetterInstance |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
internal class DirectPropertySetterInstance : ISetterInstance |
|||
{ |
|||
} |
|||
} |
|||
@ -1,40 +1,12 @@ |
|||
using System; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a setter that has been instanced on a control.
|
|||
/// Represents an <see cref="ISetter"/> that has been instanced on a control.
|
|||
/// </summary>
|
|||
[Unstable] |
|||
public interface ISetterInstance : IDisposable |
|||
public interface ISetterInstance |
|||
{ |
|||
/// <summary>
|
|||
/// Starts the setter instance.
|
|||
/// </summary>
|
|||
/// <param name="hasActivator">Whether the parent style has an activator.</param>
|
|||
/// <remarks>
|
|||
/// If <paramref name="hasActivator"/> is false then the setter should be immediately
|
|||
/// applied and <see cref="Activate"/> and <see cref="Deactivate"/> should not be called.
|
|||
/// If true, then bindings etc should be initiated but not produce a value until
|
|||
/// <see cref="Activate"/> called.
|
|||
/// </remarks>
|
|||
public void Start(bool hasActivator); |
|||
|
|||
/// <summary>
|
|||
/// Activates the setter.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
|
|||
/// </remarks>
|
|||
public void Activate(); |
|||
|
|||
/// <summary>
|
|||
/// Deactivates the setter.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
|
|||
/// </remarks>
|
|||
public void Deactivate(); |
|||
} |
|||
} |
|||
|
|||
@ -1,200 +1,60 @@ |
|||
using System; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
using Avalonia.Reactive; |
|||
|
|||
#nullable enable |
|||
using Avalonia.PropertyStore; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// A <see cref="Setter"/> which has been instanced on a control and has an
|
|||
/// <see cref="IBinding"/> as its value.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The target property type.</typeparam>
|
|||
internal class PropertySetterBindingInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
|||
ISubject<BindingValue<T>>, |
|||
ISetterInstance |
|||
internal class PropertySetterBindingInstance : UntypedBindingEntry, ISetterInstance |
|||
{ |
|||
private readonly IStyleable _target; |
|||
private readonly StyledPropertyBase<T>? _styledProperty; |
|||
private readonly DirectPropertyBase<T>? _directProperty; |
|||
private readonly InstancedBinding? _binding; |
|||
private readonly Inner _inner; |
|||
private BindingValue<T> _value; |
|||
private IDisposable? _subscription; |
|||
private IDisposable? _subscriptionTwoWay; |
|||
private IDisposable? _innerSubscription; |
|||
private bool _isActive; |
|||
private readonly AvaloniaObject _target; |
|||
private readonly BindingMode _mode; |
|||
|
|||
public PropertySetterBindingInstance( |
|||
IStyleable target, |
|||
StyledPropertyBase<T> property, |
|||
IBinding binding) |
|||
AvaloniaObject target, |
|||
StyleInstance instance, |
|||
AvaloniaProperty property, |
|||
BindingMode mode, |
|||
IObservable<object?> source) |
|||
: base(instance, property, source) |
|||
{ |
|||
_target = target; |
|||
_styledProperty = property; |
|||
_binding = binding.Initiate(_target, property); |
|||
_mode = mode; |
|||
|
|||
if (_binding?.Mode == BindingMode.OneTime) |
|||
if (mode == BindingMode.TwoWay && |
|||
source is not IObserver<object?>) |
|||
{ |
|||
// For the moment, we don't support OneTime bindings in setters, because I'm not
|
|||
// sure what the semantics should be in the case of activation/deactivation.
|
|||
throw new NotSupportedException("OneTime bindings are not supported in setters."); |
|||
throw new NotSupportedException( |
|||
"Attempting to bind two-way with a binding source which doesn't support it."); |
|||
} |
|||
|
|||
_inner = new Inner(this); |
|||
} |
|||
|
|||
public PropertySetterBindingInstance( |
|||
IStyleable target, |
|||
DirectPropertyBase<T> property, |
|||
IBinding binding) |
|||
public override void Unsubscribe() |
|||
{ |
|||
_target = target; |
|||
_directProperty = property; |
|||
_binding = binding.Initiate(_target, property); |
|||
_inner = new Inner(this); |
|||
_target.PropertyChanged -= PropertyChanged; |
|||
base.Unsubscribe(); |
|||
} |
|||
|
|||
public void Start(bool hasActivator) |
|||
protected override void Start(bool produceValue) |
|||
{ |
|||
if (_binding is null) |
|||
return; |
|||
|
|||
_isActive = !hasActivator; |
|||
|
|||
if (_styledProperty is object) |
|||
if (!IsSubscribed) |
|||
{ |
|||
if (_binding.Mode != BindingMode.OneWayToSource) |
|||
if (_mode == BindingMode.TwoWay) |
|||
{ |
|||
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; |
|||
_subscription = _target.Bind(_styledProperty, this, priority); |
|||
var observer = (IObserver<object?>)Source; |
|||
_target.PropertyChanged += PropertyChanged; |
|||
} |
|||
|
|||
if (_binding.Mode == BindingMode.TwoWay) |
|||
{ |
|||
_subscriptionTwoWay = _target.GetBindingObservable(_styledProperty).Subscribe(this); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (_binding.Mode != BindingMode.OneWayToSource) |
|||
{ |
|||
_subscription = _target.Bind(_directProperty!, this); |
|||
} |
|||
|
|||
if (_binding.Mode == BindingMode.TwoWay) |
|||
{ |
|||
_subscriptionTwoWay = _target.GetBindingObservable(_directProperty!).Subscribe(this); |
|||
} |
|||
base.Start(produceValue); |
|||
} |
|||
} |
|||
|
|||
public void Activate() |
|||
private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (_binding is null) |
|||
return; |
|||
|
|||
if (!_isActive) |
|||
if (e.Property == Property && e.Priority >= BindingPriority.LocalValue) |
|||
{ |
|||
_innerSubscription ??= _binding.Observable!.Subscribe(_inner); |
|||
_isActive = true; |
|||
PublishNext(); |
|||
if (Frame.Owner is not null && !Frame.Owner.IsEvaluating) |
|||
((IObserver<object?>)Source).OnNext(e.NewValue); |
|||
} |
|||
} |
|||
|
|||
public void Deactivate() |
|||
{ |
|||
if (_isActive) |
|||
{ |
|||
_isActive = false; |
|||
_innerSubscription?.Dispose(); |
|||
_innerSubscription = null; |
|||
PublishNext(); |
|||
} |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
if (_subscription is object) |
|||
{ |
|||
var sub = _subscription; |
|||
_subscription = null; |
|||
sub.Dispose(); |
|||
} |
|||
|
|||
if (_subscriptionTwoWay is object) |
|||
{ |
|||
var sub = _subscriptionTwoWay; |
|||
_subscriptionTwoWay = null; |
|||
sub.Dispose(); |
|||
} |
|||
|
|||
base.Dispose(); |
|||
} |
|||
|
|||
void IObserver<BindingValue<T>>.OnCompleted() |
|||
{ |
|||
// This is the observable coming from the target control. It should not complete.
|
|||
} |
|||
|
|||
void IObserver<BindingValue<T>>.OnError(Exception error) |
|||
{ |
|||
// This is the observable coming from the target control. It should not error.
|
|||
} |
|||
|
|||
void IObserver<BindingValue<T>>.OnNext(BindingValue<T> value) |
|||
{ |
|||
if (value.HasValue && _isActive && _binding?.Subject is not null) |
|||
{ |
|||
_binding.Subject.OnNext(value.Value); |
|||
} |
|||
} |
|||
|
|||
protected override void Subscribed() |
|||
{ |
|||
if (_isActive && _binding?.Observable is not null) |
|||
{ |
|||
if (_innerSubscription is null) |
|||
{ |
|||
_innerSubscription ??= _binding.Observable!.Subscribe(_inner); |
|||
} |
|||
else |
|||
{ |
|||
PublishNext(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void Unsubscribed() |
|||
{ |
|||
_innerSubscription?.Dispose(); |
|||
_innerSubscription = null; |
|||
} |
|||
|
|||
private void PublishNext() |
|||
{ |
|||
PublishNext(_isActive ? _value : default); |
|||
} |
|||
|
|||
private void ConvertAndPublishNext(object? value) |
|||
{ |
|||
_value = BindingValue<T>.FromUntyped(value); |
|||
|
|||
if (_isActive) |
|||
{ |
|||
PublishNext(); |
|||
} |
|||
} |
|||
|
|||
private class Inner : IObserver<object?> |
|||
{ |
|||
private readonly PropertySetterBindingInstance<T> _owner; |
|||
public Inner(PropertySetterBindingInstance<T> owner) => _owner = owner; |
|||
public void OnCompleted() => _owner.PublishCompleted(); |
|||
public void OnError(Exception error) => _owner.PublishError(error); |
|||
public void OnNext(object? value) => _owner.ConvertAndPublishNext(value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,127 +1,24 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Reactive; |
|||
|
|||
#nullable enable |
|||
using Avalonia.PropertyStore; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
|
|||
/// evaluated.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The target property type.</typeparam>
|
|||
internal class PropertySetterTemplateInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
|||
ISetterInstance |
|||
internal class PropertySetterTemplateInstance : IValueEntry, ISetterInstance |
|||
{ |
|||
private readonly IStyleable _target; |
|||
private readonly StyledPropertyBase<T>? _styledProperty; |
|||
private readonly DirectPropertyBase<T>? _directProperty; |
|||
private readonly ITemplate _template; |
|||
private BindingValue<T> _value; |
|||
private IDisposable? _subscription; |
|||
private bool _isActive; |
|||
|
|||
public PropertySetterTemplateInstance( |
|||
IStyleable target, |
|||
StyledPropertyBase<T> property, |
|||
ITemplate template) |
|||
{ |
|||
_target = target; |
|||
_styledProperty = property; |
|||
_template = template; |
|||
} |
|||
private object? _value; |
|||
|
|||
public PropertySetterTemplateInstance( |
|||
IStyleable target, |
|||
DirectPropertyBase<T> property, |
|||
ITemplate template) |
|||
public PropertySetterTemplateInstance(AvaloniaProperty property, ITemplate template) |
|||
{ |
|||
_target = target; |
|||
_directProperty = property; |
|||
_template = template; |
|||
Property = property; |
|||
} |
|||
|
|||
public void Start(bool hasActivator) |
|||
{ |
|||
_isActive = !hasActivator; |
|||
|
|||
if (_styledProperty is not null) |
|||
{ |
|||
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; |
|||
_subscription = _target.Bind(_styledProperty, this, priority); |
|||
} |
|||
else |
|||
{ |
|||
_subscription = _target.Bind(_directProperty!, this); |
|||
} |
|||
} |
|||
|
|||
public void Activate() |
|||
{ |
|||
if (!_isActive) |
|||
{ |
|||
_isActive = true; |
|||
PublishNext(); |
|||
} |
|||
} |
|||
|
|||
public void Deactivate() |
|||
{ |
|||
if (_isActive) |
|||
{ |
|||
_isActive = false; |
|||
PublishNext(); |
|||
} |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
if (_subscription is not null) |
|||
{ |
|||
var sub = _subscription; |
|||
_subscription = null; |
|||
sub.Dispose(); |
|||
} |
|||
else if (_isActive) |
|||
{ |
|||
if (_styledProperty is not null) |
|||
{ |
|||
_target.ClearValue(_styledProperty); |
|||
} |
|||
else |
|||
{ |
|||
_target.ClearValue(_directProperty!); |
|||
} |
|||
} |
|||
|
|||
base.Dispose(); |
|||
} |
|||
public bool HasValue => true; |
|||
public AvaloniaProperty Property { get; } |
|||
|
|||
protected override void Subscribed() => PublishNext(); |
|||
protected override void Unsubscribed() { } |
|||
public object? GetValue() => _value ??= _template.Build(); |
|||
|
|||
private void EnsureTemplate() |
|||
{ |
|||
if (_value.HasValue) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_value = (T) _template.Build(); |
|||
} |
|||
|
|||
private void PublishNext() |
|||
{ |
|||
if (_isActive) |
|||
{ |
|||
EnsureTemplate(); |
|||
PublishNext(_value); |
|||
} |
|||
else |
|||
{ |
|||
PublishNext(default); |
|||
} |
|||
} |
|||
void IValueEntry.Unsubscribe() { } |
|||
} |
|||
} |
|||
|
|||
@ -1,137 +1,103 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Data; |
|||
using Avalonia.PropertyStore; |
|||
using Avalonia.Styling.Activators; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// A <see cref="Style"/> which has been instanced on a control.
|
|||
/// Stores state for a <see cref="Style"/> that has been instanced on a control.
|
|||
/// </summary>
|
|||
internal sealed class StyleInstance : IStyleInstance, IStyleActivatorSink |
|||
/// <remarks>
|
|||
/// <see cref="StyleInstance"/> is based on <see cref="ValueFrame"/> meaning that it is
|
|||
/// injected directly into the value store of an <see cref="AvaloniaObject"/>. Depending on
|
|||
/// the setters present on the style, it may be possible to share a single style instance
|
|||
/// among all controls that the style is applied to, meaning that a single style instance can
|
|||
/// apply to multiple controls.
|
|||
/// </remarks>
|
|||
internal class StyleInstance : ValueFrame, IStyleInstance, IStyleActivatorSink, IDisposable |
|||
{ |
|||
private readonly ISetterInstance[]? _setters; |
|||
private readonly IDisposable[]? _animations; |
|||
private readonly IStyleActivator? _activator; |
|||
private readonly Subject<bool>? _animationTrigger; |
|||
private bool _isActive; |
|||
private List<ISetterInstance>? _setters; |
|||
private List<IAnimation>? _animations; |
|||
private Subject<bool>? _animationTrigger; |
|||
|
|||
public StyleInstance( |
|||
IStyle source, |
|||
IStyleable target, |
|||
IReadOnlyList<ISetter>? setters, |
|||
IReadOnlyList<IAnimation>? animations, |
|||
IStyleActivator? activator = null) |
|||
public StyleInstance(IStyle style, IStyleActivator? activator) |
|||
{ |
|||
Source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
Target = target ?? throw new ArgumentNullException(nameof(target)); |
|||
_activator = activator; |
|||
IsActive = _activator is null; |
|||
|
|||
if (setters is not null) |
|||
{ |
|||
var setterCount = setters.Count; |
|||
|
|||
_setters = new ISetterInstance[setterCount]; |
|||
|
|||
for (var i = 0; i < setterCount; ++i) |
|||
{ |
|||
_setters[i] = setters[i].Instance(Target); |
|||
} |
|||
} |
|||
|
|||
if (animations is not null && target is Animatable animatable) |
|||
{ |
|||
var animationsCount = animations.Count; |
|||
|
|||
_animations = new IDisposable[animationsCount]; |
|||
_animationTrigger = new Subject<bool>(); |
|||
|
|||
for (var i = 0; i < animationsCount; ++i) |
|||
{ |
|||
_animations[i] = animations[i].Apply(animatable, null, _animationTrigger); |
|||
} |
|||
} |
|||
Priority = activator is object ? BindingPriority.StyleTrigger : BindingPriority.Style; |
|||
Source = style; |
|||
} |
|||
|
|||
public bool HasActivator => _activator is not null; |
|||
public bool IsActive { get; private set; } |
|||
public bool HasActivator => _activator is object; |
|||
|
|||
public IStyle Source { get; } |
|||
public IStyleable Target { get; } |
|||
|
|||
public void Start() |
|||
bool IStyleInstance.IsActive => _isActive; |
|||
|
|||
public void Add(ISetterInstance instance) |
|||
{ |
|||
var hasActivator = HasActivator; |
|||
|
|||
if (_setters is not null) |
|||
{ |
|||
foreach (var setter in _setters) |
|||
{ |
|||
setter.Start(hasActivator); |
|||
} |
|||
} |
|||
|
|||
if (hasActivator) |
|||
if (instance is IValueEntry valueEntry) |
|||
{ |
|||
_activator!.Subscribe(this, 0); |
|||
} |
|||
else if (_animationTrigger is not null) |
|||
{ |
|||
_animationTrigger.OnNext(true); |
|||
if (Contains(valueEntry.Property)) |
|||
throw new InvalidOperationException( |
|||
$"Duplicate setter encountered for property '{valueEntry.Property}' in '{Source}'."); |
|||
Add(valueEntry); |
|||
} |
|||
else |
|||
(_setters ??= new()).Add(instance); |
|||
} |
|||
|
|||
public void Dispose() |
|||
public void Add(IList<IAnimation> animations) |
|||
{ |
|||
if (_setters is not null) |
|||
{ |
|||
foreach (var setter in _setters) |
|||
{ |
|||
setter.Dispose(); |
|||
} |
|||
} |
|||
if (_animations is null) |
|||
_animations = new List<IAnimation>(animations); |
|||
else |
|||
_animations.AddRange(animations); |
|||
} |
|||
|
|||
if (_animations is not null) |
|||
public void ApplyAnimations(AvaloniaObject control) |
|||
{ |
|||
if (_animations is not null && control is Animatable animatable) |
|||
{ |
|||
foreach (var subscription in _animations) |
|||
{ |
|||
subscription.Dispose(); |
|||
} |
|||
_animationTrigger ??= new Subject<bool>(); |
|||
foreach (var animation in _animations) |
|||
animation.Apply(animatable, null, _animationTrigger); |
|||
} |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
base.Dispose(); |
|||
_activator?.Dispose(); |
|||
} |
|||
|
|||
private void ActivatorChanged(bool value) |
|||
public new void MakeShared() => base.MakeShared(); |
|||
|
|||
void IStyleActivatorSink.OnNext(bool value) |
|||
{ |
|||
if (IsActive != value) |
|||
{ |
|||
IsActive = value; |
|||
Owner?.OnFrameActivationChanged(this); |
|||
_animationTrigger?.OnNext(value); |
|||
} |
|||
|
|||
_animationTrigger?.OnNext(value); |
|||
protected override bool GetIsActive(out bool hasChanged) |
|||
{ |
|||
var previous = _isActive; |
|||
|
|||
if (_setters is not null) |
|||
{ |
|||
if (IsActive) |
|||
{ |
|||
foreach (var setter in _setters) |
|||
{ |
|||
setter.Activate(); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
foreach (var setter in _setters) |
|||
{ |
|||
setter.Deactivate(); |
|||
} |
|||
} |
|||
} |
|||
if (_activator?.IsSubscribed == false) |
|||
{ |
|||
_activator.Subscribe(this); |
|||
_animationTrigger?.OnNext(_activator.GetIsActive()); |
|||
} |
|||
} |
|||
|
|||
void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); |
|||
_isActive = _activator?.GetIsActive() ?? true; |
|||
hasChanged = _isActive != previous; |
|||
return _isActive; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,368 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Stores values with <see cref="AvaloniaProperty"/> as key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Stored value type.</typeparam>
|
|||
/// <remarks>
|
|||
/// This struct implements the most commonly-used part of the dictionary API, but does
|
|||
/// not implement <see cref="IDictionary{TKey, TValue}"/>. In particular, this struct
|
|||
/// is not enumerable. Enumeration is intended to be done by index for better performance.
|
|||
/// </remarks>
|
|||
internal struct AvaloniaPropertyDictionary<TValue> |
|||
{ |
|||
private const int DefaultInitialCapacity = 4; |
|||
private Entry[]? _entries; |
|||
private int _entryCount; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AvaloniaPropertyDictionary{TValue}"/>
|
|||
/// class that is empty and has the default initial capacity.
|
|||
/// </summary>
|
|||
public AvaloniaPropertyDictionary() |
|||
{ |
|||
_entries = null; |
|||
_entryCount = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AvaloniaPropertyDictionary{TValue}"/>
|
|||
/// class that is empty and has the specified initial capacity.
|
|||
/// </summary>
|
|||
/// <param name="capactity">
|
|||
/// The initial number of elements that the collection can contain.
|
|||
/// </param>
|
|||
public AvaloniaPropertyDictionary(int capactity) |
|||
{ |
|||
_entries = new Entry[capactity]; |
|||
_entryCount = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of key/value pairs contained in the collection.
|
|||
/// </summary>
|
|||
public int Count => _entryCount; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the value associated with the specified key.
|
|||
/// </summary>
|
|||
/// <param name="property">The key to get or set.</param>
|
|||
/// <returns>
|
|||
/// The value associated with the specified key. If the key is not found, a get operation
|
|||
/// throws a <see cref="KeyNotFoundException"/>, and a set operation creates a
|
|||
/// new element for the specified key.
|
|||
/// </returns>
|
|||
/// <exception cref="KeyNotFoundException">
|
|||
/// The key does not exist in the collection.
|
|||
/// </exception>
|
|||
public TValue this[AvaloniaProperty property] |
|||
{ |
|||
get |
|||
{ |
|||
if (!TryGetEntry(property.Id, out var index)) |
|||
ThrowNotFound(); |
|||
return _entries[index].Value; |
|||
} |
|||
set |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
_entries[index] = new Entry(property, value); |
|||
else |
|||
InsertEntry(new Entry(property, value), index); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the value at the specified index.
|
|||
/// </summary>
|
|||
/// <param name="index">
|
|||
/// The index of the entry, between 0 and <see cref="Count"/> - 1.
|
|||
/// </param>
|
|||
public TValue this[int index] |
|||
{ |
|||
get |
|||
{ |
|||
if (index >= _entryCount) |
|||
ThrowOutOfRange(); |
|||
return _entries![index].Value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds the specified key and value to the dictionary.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
/// <param name="value">The value of the element to add.</param>
|
|||
public void Add(AvaloniaProperty property, TValue value) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
ThrowDuplicate(); |
|||
InsertEntry(new Entry(property, value), index); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes all keys and values from the collection.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The Count property is set to 0, and references to other objects from elements of the
|
|||
/// collection are also released. The capacity remains unchanged.
|
|||
/// </remarks>
|
|||
public void Clear() |
|||
{ |
|||
if (_entries is not null) |
|||
{ |
|||
Array.Clear(_entries, 0, _entries.Length); |
|||
_entryCount = 0; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines whether the collection contains the specified key.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
public bool ContainsKey(AvaloniaProperty property) => TryGetEntry(property.Id, out _); |
|||
|
|||
/// <summary>
|
|||
/// Gets the key and value at the specified index.
|
|||
/// </summary>
|
|||
/// <param name="index">
|
|||
/// The index of the entry, between 0 and <see cref="Count"/> - 1.
|
|||
/// </param>
|
|||
/// <param name="key">
|
|||
/// When this method returns, contains the key at the specified index.
|
|||
/// </param>
|
|||
/// <param name="value">
|
|||
/// When this method returns, contains the value at the specified index.
|
|||
/// </param>
|
|||
public void GetKeyValue(int index, out AvaloniaProperty key, out TValue value) |
|||
{ |
|||
if (index >= _entryCount) |
|||
ThrowOutOfRange(); |
|||
ref var entry = ref _entries![index]; |
|||
key = entry.Property; |
|||
value = entry.Value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the value of the specified key from the collection.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
/// <returns>
|
|||
/// true if the element is successfully found and removed; otherwise, false. This method
|
|||
/// returns false if key is not found in the collection.
|
|||
/// </returns>
|
|||
public bool Remove(AvaloniaProperty property) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
{ |
|||
RemoveAt(index); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the value of the specified key from the collection, and copies the element to
|
|||
/// the value parameter.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
/// <param name="value">The removed element.</param>
|
|||
/// <returns>
|
|||
/// true if the element is successfully found and removed; otherwise, false. This method
|
|||
/// returns false if key is not found in the collection.
|
|||
/// </returns>
|
|||
public bool Remove(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
{ |
|||
value = _entries[index].Value; |
|||
RemoveAt(index); |
|||
return true; |
|||
} |
|||
|
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the element at the specified index from the collection.
|
|||
/// </summary>
|
|||
/// <param name="index">The index.</param>
|
|||
public void RemoveAt(int index) |
|||
{ |
|||
if (_entries is null) |
|||
throw new IndexOutOfRangeException(); |
|||
|
|||
Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1); |
|||
_entryCount--; |
|||
_entries[_entryCount] = default; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to add the specified key and value to the collection.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
/// <param name="value">The value of the element to add.</param>
|
|||
/// <returns></returns>
|
|||
public bool TryAdd(AvaloniaProperty property, TValue value) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
return false; |
|||
InsertEntry(new Entry(property, value), index); |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the value associated with the specified key.
|
|||
/// </summary>
|
|||
/// <param name="property">The property key.</param>
|
|||
/// <param name="value">
|
|||
/// When this method returns, contains the value associated with the specified key,
|
|||
/// if the property is found; otherwise, null. This parameter is passed uninitialized.
|
|||
/// </param>
|
|||
/// <returns></returns>
|
|||
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
{ |
|||
value = _entries[index].Value; |
|||
return true; |
|||
} |
|||
|
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
[MemberNotNullWhen(true, nameof(_entries))] |
|||
private bool TryGetEntry(int propertyId, out int index) |
|||
{ |
|||
int checkIndex; |
|||
int iLo = 0; |
|||
int iHi = _entryCount; |
|||
|
|||
if (iHi <= 0) |
|||
{ |
|||
index = 0; |
|||
return false; |
|||
} |
|||
|
|||
// Do a binary search to find the value
|
|||
while (iHi - iLo > 3) |
|||
{ |
|||
int iPv = (iHi + iLo) / 2; |
|||
checkIndex = _entries![iPv].Property.Id; |
|||
|
|||
if (propertyId == checkIndex) |
|||
{ |
|||
index = iPv; |
|||
return true; |
|||
} |
|||
|
|||
if (propertyId <= checkIndex) |
|||
{ |
|||
iHi = iPv; |
|||
} |
|||
else |
|||
{ |
|||
iLo = iPv + 1; |
|||
} |
|||
} |
|||
|
|||
// Now we only have three values to search; switch to a linear search
|
|||
do |
|||
{ |
|||
checkIndex = _entries![iLo].Property.Id; |
|||
|
|||
if (checkIndex == propertyId) |
|||
{ |
|||
index = iLo; |
|||
return true; |
|||
} |
|||
|
|||
if (checkIndex > propertyId) |
|||
{ |
|||
// we've gone past the targetIndex - return not found
|
|||
break; |
|||
} |
|||
|
|||
iLo++; |
|||
} while (iLo < iHi); |
|||
|
|||
index = iLo; |
|||
return false; |
|||
} |
|||
|
|||
[MemberNotNull(nameof(_entries))] |
|||
private void InsertEntry(Entry entry, int entryIndex) |
|||
{ |
|||
if (_entryCount > 0) |
|||
{ |
|||
if (_entryCount == _entries!.Length) |
|||
{ |
|||
const double growthFactor = 1.2; |
|||
var newSize = (int)(_entryCount * growthFactor); |
|||
|
|||
if (newSize == _entryCount) |
|||
{ |
|||
newSize++; |
|||
} |
|||
|
|||
var destEntries = new Entry[newSize]; |
|||
|
|||
Array.Copy(_entries, 0, destEntries, 0, entryIndex); |
|||
|
|||
destEntries[entryIndex] = entry; |
|||
|
|||
Array.Copy(_entries, entryIndex, destEntries, entryIndex + 1, _entryCount - entryIndex); |
|||
|
|||
_entries = destEntries; |
|||
} |
|||
else |
|||
{ |
|||
Array.Copy( |
|||
_entries, |
|||
entryIndex, |
|||
_entries, |
|||
entryIndex + 1, |
|||
_entryCount - entryIndex); |
|||
|
|||
_entries[entryIndex] = entry; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
_entries ??= new Entry[DefaultInitialCapacity]; |
|||
_entries[0] = entry; |
|||
} |
|||
|
|||
_entryCount++; |
|||
} |
|||
|
|||
[DoesNotReturn] |
|||
private static void ThrowOutOfRange() => throw new IndexOutOfRangeException(); |
|||
|
|||
[DoesNotReturn] |
|||
private static void ThrowDuplicate() => |
|||
throw new ArgumentException("An item with the same key has already been added."); |
|||
|
|||
[DoesNotReturn] |
|||
private static void ThrowNotFound() => throw new KeyNotFoundException(); |
|||
|
|||
private readonly struct Entry |
|||
{ |
|||
public readonly AvaloniaProperty Property; |
|||
public readonly TValue Value; |
|||
|
|||
public Entry(AvaloniaProperty property, TValue value) |
|||
{ |
|||
Property = property; |
|||
Value = value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,173 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Stores values with <see cref="AvaloniaProperty"/> as key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Stored value type.</typeparam>
|
|||
internal sealed class AvaloniaPropertyValueStore<TValue> |
|||
{ |
|||
// The last item in the list is always int.MaxValue.
|
|||
private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } }; |
|||
|
|||
private Entry[] _entries; |
|||
|
|||
public AvaloniaPropertyValueStore() |
|||
{ |
|||
_entries = s_emptyEntries; |
|||
} |
|||
|
|||
public int Count => _entries.Length - 1; |
|||
public TValue this[int index] => _entries[index].Value; |
|||
|
|||
private (int, bool) TryFindEntry(int propertyId) |
|||
{ |
|||
if (_entries.Length <= 12) |
|||
{ |
|||
// For small lists, we use an optimized linear search. Since the last item in the list
|
|||
// is always int.MaxValue, we can skip a conditional branch in each iteration.
|
|||
// By unrolling the loop, we can skip another unconditional branch in each iteration.
|
|||
|
|||
if (_entries[0].PropertyId >= propertyId) |
|||
return (0, _entries[0].PropertyId == propertyId); |
|||
if (_entries[1].PropertyId >= propertyId) |
|||
return (1, _entries[1].PropertyId == propertyId); |
|||
if (_entries[2].PropertyId >= propertyId) |
|||
return (2, _entries[2].PropertyId == propertyId); |
|||
if (_entries[3].PropertyId >= propertyId) |
|||
return (3, _entries[3].PropertyId == propertyId); |
|||
if (_entries[4].PropertyId >= propertyId) |
|||
return (4, _entries[4].PropertyId == propertyId); |
|||
if (_entries[5].PropertyId >= propertyId) |
|||
return (5, _entries[5].PropertyId == propertyId); |
|||
if (_entries[6].PropertyId >= propertyId) |
|||
return (6, _entries[6].PropertyId == propertyId); |
|||
if (_entries[7].PropertyId >= propertyId) |
|||
return (7, _entries[7].PropertyId == propertyId); |
|||
if (_entries[8].PropertyId >= propertyId) |
|||
return (8, _entries[8].PropertyId == propertyId); |
|||
if (_entries[9].PropertyId >= propertyId) |
|||
return (9, _entries[9].PropertyId == propertyId); |
|||
if (_entries[10].PropertyId >= propertyId) |
|||
return (10, _entries[10].PropertyId == propertyId); |
|||
} |
|||
else |
|||
{ |
|||
int low = 0; |
|||
int high = _entries.Length; |
|||
int id; |
|||
|
|||
while (high - low > 3) |
|||
{ |
|||
int pivot = (high + low) / 2; |
|||
id = _entries[pivot].PropertyId; |
|||
|
|||
if (propertyId == id) |
|||
return (pivot, true); |
|||
|
|||
if (propertyId <= id) |
|||
high = pivot; |
|||
else |
|||
low = pivot + 1; |
|||
} |
|||
|
|||
do |
|||
{ |
|||
id = _entries[low].PropertyId; |
|||
|
|||
if (id == propertyId) |
|||
return (low, true); |
|||
|
|||
if (id > propertyId) |
|||
break; |
|||
|
|||
++low; |
|||
} |
|||
while (low < high); |
|||
} |
|||
|
|||
return (0, false); |
|||
} |
|||
|
|||
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) |
|||
{ |
|||
(int index, bool found) = TryFindEntry(property.Id); |
|||
if (!found) |
|||
{ |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
value = _entries[index].Value; |
|||
return true; |
|||
} |
|||
|
|||
public void AddValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
Entry[] entries = new Entry[_entries.Length + 1]; |
|||
|
|||
for (int i = 0; i < _entries.Length; ++i) |
|||
{ |
|||
if (_entries[i].PropertyId > property.Id) |
|||
{ |
|||
if (i > 0) |
|||
{ |
|||
Array.Copy(_entries, 0, entries, 0, i); |
|||
} |
|||
|
|||
entries[i] = new Entry { PropertyId = property.Id, Value = value }; |
|||
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
_entries = entries; |
|||
} |
|||
|
|||
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) |
|||
{ |
|||
var newLength = _entries.Length - 1; |
|||
|
|||
// Special case - one element left means that value store is empty so we can just reuse our "empty" array.
|
|||
if (newLength == 1) |
|||
{ |
|||
_entries = s_emptyEntries; |
|||
|
|||
return; |
|||
} |
|||
|
|||
var entries = new Entry[newLength]; |
|||
|
|||
int ix = 0; |
|||
|
|||
for (int i = 0; i < _entries.Length; ++i) |
|||
{ |
|||
if (i != index) |
|||
{ |
|||
entries[ix++] = _entries[i]; |
|||
} |
|||
} |
|||
|
|||
_entries = entries; |
|||
} |
|||
} |
|||
|
|||
private struct Entry |
|||
{ |
|||
internal int PropertyId; |
|||
internal TValue Value; |
|||
} |
|||
} |
|||
} |
|||
@ -1,507 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
using Avalonia.PropertyStore; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Stores styled property values for an <see cref="AvaloniaObject"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// At its core this class consists of an <see cref="AvaloniaProperty"/> to
|
|||
/// <see cref="IValue"/> mapping which holds the current values for each set property. This
|
|||
/// <see cref="IValue"/> can be in one of 4 states:
|
|||
///
|
|||
/// - For a single local value it will be an instance of <see cref="LocalValueEntry{T}"/>.
|
|||
/// - For a single value of a priority other than LocalValue it will be an instance of
|
|||
/// <see cref="ConstantValueEntry{T}"/>`
|
|||
/// - For a single binding it will be an instance of <see cref="BindingEntry{T}"/>
|
|||
/// - For all other cases it will be an instance of <see cref="PriorityValue{T}"/>
|
|||
/// </remarks>
|
|||
internal class ValueStore |
|||
{ |
|||
private readonly AvaloniaObject _owner; |
|||
private readonly AvaloniaPropertyValueStore<IValue> _values; |
|||
private BatchUpdate? _batchUpdate; |
|||
|
|||
public ValueStore(AvaloniaObject owner) |
|||
{ |
|||
_owner = owner; |
|||
_values = new AvaloniaPropertyValueStore<IValue>(); |
|||
} |
|||
|
|||
public void BeginBatchUpdate() |
|||
{ |
|||
_batchUpdate ??= new BatchUpdate(this); |
|||
_batchUpdate.Begin(); |
|||
} |
|||
|
|||
public void EndBatchUpdate() |
|||
{ |
|||
if (_batchUpdate is null) |
|||
{ |
|||
throw new InvalidOperationException("No batch update in progress."); |
|||
} |
|||
|
|||
if (_batchUpdate.End()) |
|||
{ |
|||
_batchUpdate = null; |
|||
} |
|||
} |
|||
|
|||
public bool IsAnimating(AvaloniaProperty property) |
|||
{ |
|||
if (TryGetValue(property, out var slot)) |
|||
{ |
|||
return slot.Priority < BindingPriority.LocalValue; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public bool IsSet(AvaloniaProperty property) |
|||
{ |
|||
if (TryGetValue(property, out var slot)) |
|||
{ |
|||
return slot.GetValue().HasValue; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public bool TryGetValue<T>( |
|||
StyledPropertyBase<T> property, |
|||
BindingPriority maxPriority, |
|||
out T value) |
|||
{ |
|||
if (TryGetValue(property, out var slot)) |
|||
{ |
|||
var v = ((IValue<T>)slot).GetValue(maxPriority); |
|||
|
|||
if (v.HasValue) |
|||
{ |
|||
value = v.Value; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
value = default!; |
|||
return false; |
|||
} |
|||
|
|||
public IDisposable? SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority) |
|||
{ |
|||
if (property.ValidateValue?.Invoke(value) == false) |
|||
{ |
|||
throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); |
|||
} |
|||
|
|||
IDisposable? result = null; |
|||
|
|||
if (TryGetValue(property, out var slot)) |
|||
{ |
|||
result = 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<T>(_owner, property, this); |
|||
AddValue(property, entry); |
|||
result = entry.SetValue(value, priority); |
|||
} |
|||
else |
|||
{ |
|||
if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
AddValue(property, new LocalValueEntry<T>(value)); |
|||
NotifyValueChanged<T>(property, default, value, priority); |
|||
} |
|||
else |
|||
{ |
|||
var entry = new ConstantValueEntry<T>(property, value, priority, new(this)); |
|||
AddValue(property, entry); |
|||
NotifyValueChanged<T>(property, default, value, priority); |
|||
result = entry; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public IDisposable AddBinding<T>( |
|||
StyledPropertyBase<T> property, |
|||
IObservable<BindingValue<T>> source, |
|||
BindingPriority priority) |
|||
{ |
|||
if (TryGetValue(property, out var slot)) |
|||
{ |
|||
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<T>(_owner, property, this); |
|||
var binding = entry.AddBinding(source, priority); |
|||
AddValue(property, entry); |
|||
return binding; |
|||
} |
|||
else |
|||
{ |
|||
var entry = new BindingEntry<T>(_owner, property, source, priority, new(this)); |
|||
AddValue(property, entry); |
|||
return entry; |
|||
} |
|||
} |
|||
|
|||
public void ClearLocalValue<T>(StyledPropertyBase<T> property) |
|||
{ |
|||
if (TryGetValue(property, out var slot)) |
|||
{ |
|||
if (slot is PriorityValue<T> p) |
|||
{ |
|||
p.ClearLocalValue(); |
|||
} |
|||
else if (slot.Priority == BindingPriority.LocalValue) |
|||
{ |
|||
var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? |
|||
new Optional<T>(value) : default; |
|||
|
|||
// During batch update values can't be removed immediately because they're needed to raise
|
|||
// a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
|
|||
// by setting their priority to Unset.
|
|||
if (!IsBatchUpdating()) |
|||
{ |
|||
_values.Remove(property); |
|||
} |
|||
else if (slot is IDisposable d) |
|||
{ |
|||
d.Dispose(); |
|||
} |
|||
else |
|||
{ |
|||
// Local value entries are optimized and contain only a single value field to save space,
|
|||
// so there's no way to mark them for removal at the end of a batch update. Instead convert
|
|||
// them to a constant value entry with Unset priority in the event of a local value being
|
|||
// cleared during a batch update.
|
|||
var sentinel = new ConstantValueEntry<T>(property, Optional<T>.Empty, BindingPriority.Unset, new(this)); |
|||
_values.SetValue(property, sentinel); |
|||
} |
|||
|
|||
NotifyValueChanged<T>(property, old, default, BindingPriority.Unset); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void CoerceValue(AvaloniaProperty property) |
|||
{ |
|||
if (TryGetValue(property, out var slot)) |
|||
{ |
|||
if (slot is IPriorityValue p) |
|||
{ |
|||
p.UpdateEffectiveValue(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) |
|||
{ |
|||
if (TryGetValue(property, out var slot)) |
|||
{ |
|||
var slotValue = slot.GetValue(); |
|||
return new Diagnostics.AvaloniaPropertyValue( |
|||
property, |
|||
slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue, |
|||
slot.Priority, |
|||
null); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) |
|||
{ |
|||
if (_batchUpdate is object) |
|||
{ |
|||
if (change.IsEffectiveValueChange) |
|||
{ |
|||
NotifyValueChanged<T>(change.Property, change.OldValue, change.NewValue, change.Priority); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
_owner.ValueChanged(change); |
|||
} |
|||
} |
|||
|
|||
public void Completed<T>( |
|||
StyledPropertyBase<T> property, |
|||
IPriorityValueEntry entry, |
|||
Optional<T> oldValue) |
|||
{ |
|||
// We need to include remove sentinels here so call `_values.TryGetValue` directly.
|
|||
if (_values.TryGetValue(property, out var slot) && slot == entry) |
|||
{ |
|||
if (_batchUpdate is null) |
|||
{ |
|||
_values.Remove(property); |
|||
_owner.Completed(property, entry, oldValue); |
|||
} |
|||
else |
|||
{ |
|||
_batchUpdate.ValueChanged(property, oldValue.ToObject()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private IDisposable? SetExisting<T>( |
|||
object slot, |
|||
StyledPropertyBase<T> property, |
|||
T value, |
|||
BindingPriority priority) |
|||
{ |
|||
IDisposable? result = null; |
|||
|
|||
if (slot is IPriorityValueEntry<T> e) |
|||
{ |
|||
var priorityValue = new PriorityValue<T>(_owner, property, this, e); |
|||
_values.SetValue(property, priorityValue); |
|||
result = priorityValue.SetValue(value, priority); |
|||
} |
|||
else if (slot is PriorityValue<T> p) |
|||
{ |
|||
result = p.SetValue(value, priority); |
|||
} |
|||
else if (slot is LocalValueEntry<T> l) |
|||
{ |
|||
if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
var old = l.GetValue(BindingPriority.LocalValue); |
|||
l.SetValue(value); |
|||
NotifyValueChanged<T>(property, old, value, priority); |
|||
} |
|||
else |
|||
{ |
|||
var priorityValue = new PriorityValue<T>(_owner, property, this, l); |
|||
if (IsBatchUpdating()) |
|||
priorityValue.BeginBatchUpdate(); |
|||
result = priorityValue.SetValue(value, priority); |
|||
_values.SetValue(property, priorityValue); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw new NotSupportedException("Unrecognised value store slot type."); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private IDisposable BindExisting<T>( |
|||
object slot, |
|||
StyledPropertyBase<T> property, |
|||
IObservable<BindingValue<T>> source, |
|||
BindingPriority priority) |
|||
{ |
|||
PriorityValue<T> priorityValue; |
|||
|
|||
if (slot is IPriorityValueEntry<T> e) |
|||
{ |
|||
priorityValue = new PriorityValue<T>(_owner, property, this, e); |
|||
|
|||
if (IsBatchUpdating()) |
|||
{ |
|||
priorityValue.BeginBatchUpdate(); |
|||
} |
|||
} |
|||
else if (slot is PriorityValue<T> p) |
|||
{ |
|||
priorityValue = p; |
|||
} |
|||
else if (slot is LocalValueEntry<T> l) |
|||
{ |
|||
priorityValue = new PriorityValue<T>(_owner, property, this, l); |
|||
} |
|||
else |
|||
{ |
|||
throw new NotSupportedException("Unrecognised value store slot type."); |
|||
} |
|||
|
|||
var binding = priorityValue.AddBinding(source, priority); |
|||
_values.SetValue(property, priorityValue); |
|||
priorityValue.UpdateEffectiveValue(); |
|||
return binding; |
|||
} |
|||
|
|||
private void AddValue(AvaloniaProperty property, IValue value) |
|||
{ |
|||
_values.AddValue(property, value); |
|||
if (IsBatchUpdating() && value is IBatchUpdate batch) |
|||
batch.BeginBatchUpdate(); |
|||
value.Start(); |
|||
} |
|||
|
|||
private void NotifyValueChanged<T>( |
|||
AvaloniaProperty<T> property, |
|||
Optional<T> oldValue, |
|||
BindingValue<T> newValue, |
|||
BindingPriority priority) |
|||
{ |
|||
if (_batchUpdate is null) |
|||
{ |
|||
_owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( |
|||
_owner, |
|||
property, |
|||
oldValue, |
|||
newValue, |
|||
priority)); |
|||
} |
|||
else |
|||
{ |
|||
_batchUpdate.ValueChanged(property, oldValue.ToObject()); |
|||
} |
|||
} |
|||
|
|||
private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true; |
|||
|
|||
private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value) |
|||
{ |
|||
return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value); |
|||
} |
|||
|
|||
private static bool IsRemoveSentinel(IValue value) |
|||
{ |
|||
// Local value entries are optimized and contain only a single value field to save space,
|
|||
// so there's no way to mark them for removal at the end of a batch update. Instead a
|
|||
// ConstantValueEntry with a priority of Unset is used as a sentinel value.
|
|||
return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset; |
|||
} |
|||
|
|||
private class BatchUpdate |
|||
{ |
|||
private ValueStore _owner; |
|||
private List<Notification>? _notifications; |
|||
private int _batchUpdateCount; |
|||
private int _iterator = -1; |
|||
|
|||
public BatchUpdate(ValueStore owner) => _owner = owner; |
|||
|
|||
public bool IsBatchUpdating => _batchUpdateCount > 0; |
|||
|
|||
public void Begin() |
|||
{ |
|||
if (_batchUpdateCount++ == 0) |
|||
{ |
|||
var values = _owner._values; |
|||
|
|||
for (var i = 0; i < values.Count; ++i) |
|||
{ |
|||
(values[i] as IBatchUpdate)?.BeginBatchUpdate(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool End() |
|||
{ |
|||
if (--_batchUpdateCount > 0) |
|||
return false; |
|||
|
|||
var values = _owner._values; |
|||
|
|||
// First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
|
|||
// but notifications will still not be raised because the owner ValueStore will still have a reference
|
|||
// to this batch update object.
|
|||
for (var i = 0; i < values.Count; ++i) |
|||
{ |
|||
(values[i] as IBatchUpdate)?.EndBatchUpdate(); |
|||
|
|||
// Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
|
|||
// does, abort and continue batch updating.
|
|||
if (_batchUpdateCount > 0) |
|||
return false; |
|||
} |
|||
|
|||
if (_notifications is object) |
|||
{ |
|||
// Raise all batched notifications. Doing this can cause other notifications to be added and even
|
|||
// cause a new batch update to start, so we need to handle _notifications being modified by storing
|
|||
// the index in field.
|
|||
_iterator = 0; |
|||
|
|||
for (; _iterator < _notifications.Count; ++_iterator) |
|||
{ |
|||
var entry = _notifications[_iterator]; |
|||
|
|||
if (values.TryGetValue(entry.property, out var slot)) |
|||
{ |
|||
var oldValue = entry.oldValue; |
|||
var newValue = slot.GetValue(); |
|||
|
|||
// Raising this notification can cause a new batch update to be started, which in turn
|
|||
// results in another change to the property. In this case we need to update the old value
|
|||
// so that the *next* notification has an oldValue which follows on from the newValue
|
|||
// raised here.
|
|||
_notifications[_iterator] = new Notification |
|||
{ |
|||
property = entry.property, |
|||
oldValue = newValue, |
|||
}; |
|||
|
|||
// Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>.
|
|||
slot.RaiseValueChanged(_owner._owner, entry.property, oldValue, newValue); |
|||
|
|||
// During batch update values can't be removed immediately because they're needed to raise
|
|||
// the _sink.ValueChanged notification. They instead mark themselves for removal by setting
|
|||
// their priority to Unset. We need to re-read the slot here because raising ValueChanged
|
|||
// could have caused it to be updated.
|
|||
if (values.TryGetValue(entry.property, out var updatedSlot) && |
|||
updatedSlot.Priority == BindingPriority.Unset) |
|||
{ |
|||
values.Remove(entry.property); |
|||
} |
|||
} |
|||
|
|||
// If a new batch update was started while ending this one, abort.
|
|||
if (_batchUpdateCount > 0) |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
_iterator = int.MaxValue - 1; |
|||
return true; |
|||
} |
|||
|
|||
public void ValueChanged(AvaloniaProperty property, Optional<object?> oldValue) |
|||
{ |
|||
_notifications ??= new List<Notification>(); |
|||
|
|||
for (var i = 0; i < _notifications.Count; ++i) |
|||
{ |
|||
if (_notifications[i].property == property) |
|||
{ |
|||
oldValue = _notifications[i].oldValue; |
|||
_notifications.RemoveAt(i); |
|||
|
|||
if (i <= _iterator) |
|||
--_iterator; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
_notifications.Add(new Notification |
|||
{ |
|||
property = property, |
|||
oldValue = oldValue, |
|||
}); |
|||
} |
|||
|
|||
private struct Notification |
|||
{ |
|||
public AvaloniaProperty property; |
|||
public Optional<object?> oldValue; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,695 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reactive; |
|||
using System.Reactive.Disposables; |
|||
using System.Reactive.Linq; |
|||
using System.Text; |
|||
using Avalonia.Data; |
|||
using Avalonia.Layout; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests |
|||
{ |
|||
public class AvaloniaObjectTests_BatchUpdate |
|||
{ |
|||
[Fact] |
|||
public void SetValue_Should_Not_Raise_Property_Changes_During_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<string>(); |
|||
|
|||
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); |
|||
target.BeginBatchUpdate(); |
|||
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
|||
|
|||
Assert.Empty(raised); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Should_Not_Raise_Property_Changes_During_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<string>("foo"); |
|||
var raised = new List<string>(); |
|||
|
|||
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); |
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
|||
|
|||
Assert.Empty(raised); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Completion_Should_Not_Raise_Property_Changes_During_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<string>("foo"); |
|||
var raised = new List<string>(); |
|||
|
|||
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
|||
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); |
|||
target.BeginBatchUpdate(); |
|||
observable.OnCompleted(); |
|||
|
|||
Assert.Empty(raised); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<string>("foo"); |
|||
var raised = new List<string>(); |
|||
|
|||
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
|||
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); |
|||
target.BeginBatchUpdate(); |
|||
sub.Dispose(); |
|||
|
|||
Assert.Empty(raised); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Equal("foo", target.Foo); |
|||
Assert.Null(raised[0].OldValue); |
|||
Assert.Equal("foo", raised[0].NewValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_2() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.SetValue(TestClass.FooProperty, "bar", BindingPriority.LocalValue); |
|||
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Equal("baz", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_3() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.SetValue(TestClass.BazProperty, Orientation.Horizontal, BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Equal(TestClass.BazProperty, raised[0].Property); |
|||
Assert.Equal(Orientation.Vertical, raised[0].OldValue); |
|||
Assert.Equal(Orientation.Horizontal, raised[0].NewValue); |
|||
Assert.Equal(Orientation.Horizontal, target.Baz); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
|||
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); |
|||
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(2, raised.Count); |
|||
Assert.Equal(TestClass.BarProperty, raised[0].Property); |
|||
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
|||
Assert.Equal("baz", target.Foo); |
|||
Assert.Equal("bar", target.Bar); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_1() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<string>("baz"); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
|||
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); |
|||
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(2, raised.Count); |
|||
Assert.Equal(TestClass.BarProperty, raised[0].Property); |
|||
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
|||
Assert.Equal("baz", target.Foo); |
|||
Assert.Equal("bar", target.Bar); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_2() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<string>("foo"); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
|||
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); |
|||
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(2, raised.Count); |
|||
Assert.Equal(TestClass.BarProperty, raised[0].Property); |
|||
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
|||
Assert.Equal("baz", target.Foo); |
|||
Assert.Equal("bar", target.Bar); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_3() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable1 = new TestObservable<string>("foo"); |
|||
var observable2 = new TestObservable<string>("qux"); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); |
|||
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
|||
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); |
|||
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(2, raised.Count); |
|||
Assert.Equal(TestClass.BarProperty, raised[0].Property); |
|||
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
|||
Assert.Equal("baz", target.Foo); |
|||
Assert.Equal("bar", target.Bar); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Change_Should_Be_Raised_After_Batch_Update_1() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<string>("foo"); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Equal("foo", target.Foo); |
|||
Assert.Null(raised[0].OldValue); |
|||
Assert.Equal("foo", raised[0].NewValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Change_Should_Be_Raised_After_Batch_Update_2() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable1 = new TestObservable<string>("bar"); |
|||
var observable2 = new TestObservable<string>("baz"); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
|||
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Equal("baz", target.Foo); |
|||
Assert.Equal("foo", raised[0].OldValue); |
|||
Assert.Equal("baz", raised[0].NewValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Change_Should_Be_Raised_After_Batch_Update_3() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<Orientation>(Orientation.Horizontal); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.BazProperty, observable, BindingPriority.LocalValue); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Equal(TestClass.BazProperty, raised[0].Property); |
|||
Assert.Equal(Orientation.Vertical, raised[0].OldValue); |
|||
Assert.Equal(Orientation.Horizontal, raised[0].NewValue); |
|||
Assert.Equal(Orientation.Horizontal, target.Baz); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Completion_Should_Be_Raised_After_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<string>("foo"); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
observable.OnCompleted(); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Null(target.Foo); |
|||
Assert.Equal("foo", raised[0].OldValue); |
|||
Assert.Null(raised[0].NewValue); |
|||
Assert.Equal(BindingPriority.Unset, raised[0].Priority); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Disposal_Should_Be_Raised_After_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable = new TestObservable<string>("foo"); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
sub.Dispose(); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Null(target.Foo); |
|||
Assert.Equal("foo", raised[0].OldValue); |
|||
Assert.Null(raised[0].NewValue); |
|||
Assert.Equal(BindingPriority.Unset, raised[0].Priority); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.Foo = "foo"; |
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.ClearValue(TestClass.FooProperty); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, raised.Count); |
|||
Assert.Null(target.Foo); |
|||
Assert.Equal("foo", raised[0].OldValue); |
|||
Assert.Null(raised[0].NewValue); |
|||
Assert.Equal(BindingPriority.Unset, raised[0].Priority); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Bindings_Should_Be_Subscribed_Before_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable1 = new TestObservable<string>("foo"); |
|||
var observable2 = new TestObservable<string>("bar"); |
|||
|
|||
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
|||
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); |
|||
|
|||
Assert.Equal(1, observable1.SubscribeCount); |
|||
Assert.Equal(1, observable2.SubscribeCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Non_Active_Binding_Should_Not_Be_Subscribed_Before_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable1 = new TestObservable<string>("foo"); |
|||
var observable2 = new TestObservable<string>("bar"); |
|||
|
|||
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
|||
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); |
|||
|
|||
Assert.Equal(1, observable1.SubscribeCount); |
|||
Assert.Equal(0, observable2.SubscribeCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void LocalValue_Bindings_Should_Be_Subscribed_During_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable1 = new TestObservable<string>("foo"); |
|||
var observable2 = new TestObservable<string>("bar"); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
// We need to subscribe to LocalValue bindings even if we've got a batch operation
|
|||
// in progress because otherwise we don't know whether the binding or a subsequent
|
|||
// SetValue with local priority will win. Notifications however shouldn't be sent.
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
|||
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); |
|||
|
|||
Assert.Equal(1, observable1.SubscribeCount); |
|||
Assert.Equal(1, observable2.SubscribeCount); |
|||
Assert.Empty(raised); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Style_Bindings_Should_Not_Be_Subscribed_During_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable1 = new TestObservable<string>("foo"); |
|||
var observable2 = new TestObservable<string>("bar"); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style); |
|||
target.Bind(TestClass.FooProperty, observable2, BindingPriority.StyleTrigger); |
|||
|
|||
Assert.Equal(0, observable1.SubscribeCount); |
|||
Assert.Equal(0, observable2.SubscribeCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_1() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable1 = new TestObservable<string>("foo"); |
|||
var observable2 = new TestObservable<string>("bar"); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style); |
|||
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(0, observable1.SubscribeCount); |
|||
Assert.Equal(1, observable2.SubscribeCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_2() |
|||
{ |
|||
var target = new TestClass(); |
|||
var observable1 = new TestObservable<string>("foo"); |
|||
var observable2 = new TestObservable<string>("bar"); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Bind(TestClass.FooProperty, observable1, BindingPriority.StyleTrigger); |
|||
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal(1, observable1.SubscribeCount); |
|||
Assert.Equal(0, observable2.SubscribeCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_1() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Foo = "foo"; |
|||
|
|||
target.PropertyChanged += (s, e) => |
|||
{ |
|||
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") |
|||
target.Bar = "bar"; |
|||
}; |
|||
|
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal("foo", target.Foo); |
|||
Assert.Equal("bar", target.Bar); |
|||
Assert.Equal(2, raised.Count); |
|||
Assert.Equal(TestClass.FooProperty, raised[0].Property); |
|||
Assert.Equal(TestClass.BarProperty, raised[1].Property); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_2() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Foo = "foo"; |
|||
target.Bar = "baz"; |
|||
|
|||
target.PropertyChanged += (s, e) => |
|||
{ |
|||
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") |
|||
target.Bar = "bar"; |
|||
}; |
|||
|
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal("foo", target.Foo); |
|||
Assert.Equal("bar", target.Bar); |
|||
Assert.Equal(2, raised.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Batch_Update_Can_Be_Triggered_By_Ending_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.PropertyChanged += (s, e) => raised.Add(e); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.Foo = "foo"; |
|||
target.Bar = "baz"; |
|||
|
|||
// Simulates the following scenario:
|
|||
// - A control is added to the logical tree
|
|||
// - A batch update is started to apply styles
|
|||
// - Ending the batch update triggers something which removes the control from the logical tree
|
|||
// - A new batch update is started to detach styles
|
|||
target.PropertyChanged += (s, e) => |
|||
{ |
|||
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") |
|||
{ |
|||
target.BeginBatchUpdate(); |
|||
target.ClearValue(TestClass.FooProperty); |
|||
target.ClearValue(TestClass.BarProperty); |
|||
target.EndBatchUpdate(); |
|||
} |
|||
}; |
|||
|
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Null(target.Foo); |
|||
Assert.Null(target.Bar); |
|||
Assert.Equal(2, raised.Count); |
|||
Assert.Equal(TestClass.FooProperty, raised[0].Property); |
|||
Assert.Null(raised[0].OldValue); |
|||
Assert.Equal("foo", raised[0].NewValue); |
|||
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
|||
Assert.Equal("foo", raised[1].OldValue); |
|||
Assert.Null(raised[1].NewValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Can_Set_Cleared_Value_When_Ending_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = 0; |
|||
|
|||
target.Foo = "foo"; |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.ClearValue(TestClass.FooProperty); |
|||
target.PropertyChanged += (sender, e) => |
|||
{ |
|||
if (e.Property == TestClass.FooProperty && e.NewValue is null) |
|||
{ |
|||
target.Foo = "bar"; |
|||
++raised; |
|||
} |
|||
}; |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal("bar", target.Foo); |
|||
Assert.Equal(1, raised); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Can_Bind_Cleared_Value_When_Ending_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = 0; |
|||
var notifications = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.Foo = "foo"; |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.ClearValue(TestClass.FooProperty); |
|||
target.PropertyChanged += (sender, e) => |
|||
{ |
|||
if (e.Property == TestClass.FooProperty && e.NewValue is null) |
|||
{ |
|||
target.Bind(TestClass.FooProperty, new TestObservable<string>("bar")); |
|||
++raised; |
|||
} |
|||
|
|||
notifications.Add(e); |
|||
}; |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal("bar", target.Foo); |
|||
Assert.Equal(1, raised); |
|||
Assert.Equal(2, notifications.Count); |
|||
Assert.Equal(null, notifications[0].NewValue); |
|||
Assert.Equal("bar", notifications[1].NewValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = 0; |
|||
var notifications = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
var observable1 = new TestObservable<string>("foo"); |
|||
var observable2 = new TestObservable<string>("foo"); |
|||
|
|||
target.Bind(TestClass.FooProperty, observable1); |
|||
|
|||
target.BeginBatchUpdate(); |
|||
observable1.OnCompleted(); |
|||
target.PropertyChanged += (sender, e) => |
|||
{ |
|||
if (e.Property == TestClass.FooProperty && e.NewValue is null) |
|||
{ |
|||
target.Bind(TestClass.FooProperty, observable2); |
|||
++raised; |
|||
} |
|||
|
|||
notifications.Add(e); |
|||
}; |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Equal("foo", target.Foo); |
|||
Assert.Equal(1, raised); |
|||
Assert.Equal(2, notifications.Count); |
|||
Assert.Equal(null, notifications[0].NewValue); |
|||
Assert.Equal("foo", notifications[1].NewValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Can_Run_Empty_Batch_Update_When_Ending_Batch_Update() |
|||
{ |
|||
var target = new TestClass(); |
|||
var raised = 0; |
|||
var notifications = new List<AvaloniaPropertyChangedEventArgs>(); |
|||
|
|||
target.Foo = "foo"; |
|||
target.Bar = "bar"; |
|||
|
|||
target.BeginBatchUpdate(); |
|||
target.ClearValue(TestClass.FooProperty); |
|||
target.ClearValue(TestClass.BarProperty); |
|||
target.PropertyChanged += (sender, e) => |
|||
{ |
|||
if (e.Property == TestClass.BarProperty) |
|||
{ |
|||
target.BeginBatchUpdate(); |
|||
target.EndBatchUpdate(); |
|||
} |
|||
|
|||
++raised; |
|||
}; |
|||
target.EndBatchUpdate(); |
|||
|
|||
Assert.Null(target.Foo); |
|||
Assert.Null(target.Bar); |
|||
Assert.Equal(2, raised); |
|||
} |
|||
|
|||
public class TestClass : AvaloniaObject |
|||
{ |
|||
public static readonly StyledProperty<string> FooProperty = |
|||
AvaloniaProperty.Register<TestClass, string>(nameof(Foo)); |
|||
|
|||
public static readonly StyledProperty<string> BarProperty = |
|||
AvaloniaProperty.Register<TestClass, string>(nameof(Bar)); |
|||
|
|||
public static readonly StyledProperty<Orientation> BazProperty = |
|||
AvaloniaProperty.Register<TestClass, Orientation>(nameof(Bar), Orientation.Vertical); |
|||
|
|||
public string Foo |
|||
{ |
|||
get => GetValue(FooProperty); |
|||
set => SetValue(FooProperty, value); |
|||
} |
|||
|
|||
public string Bar |
|||
{ |
|||
get => GetValue(BarProperty); |
|||
set => SetValue(BarProperty, value); |
|||
} |
|||
|
|||
public Orientation Baz |
|||
{ |
|||
get => GetValue(BazProperty); |
|||
set => SetValue(BazProperty, value); |
|||
} |
|||
} |
|||
|
|||
public class TestObservable<T> : ObservableBase<BindingValue<T>> |
|||
{ |
|||
private readonly T _value; |
|||
private IObserver<BindingValue<T>> _observer; |
|||
|
|||
public TestObservable(T value) => _value = value; |
|||
|
|||
public int SubscribeCount { get; private set; } |
|||
|
|||
public void OnCompleted() => _observer.OnCompleted(); |
|||
public void OnError(Exception e) => _observer.OnError(e); |
|||
|
|||
protected override IDisposable SubscribeCore(IObserver<BindingValue<T>> observer) |
|||
{ |
|||
++SubscribeCount; |
|||
_observer = observer; |
|||
observer.OnNext(_value); |
|||
return Disposable.Empty; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,314 +0,0 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Data; |
|||
using Avalonia.PropertyStore; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests |
|||
{ |
|||
public class PriorityValueTests |
|||
{ |
|||
private static readonly AvaloniaObject Owner = new AvaloniaObject(); |
|||
private static readonly ValueStore ValueStore = new ValueStore(Owner); |
|||
private static readonly StyledProperty<string> TestProperty = new StyledProperty<string>( |
|||
"Test", |
|||
typeof(PriorityValueTests), |
|||
new StyledPropertyMetadata<string>()); |
|||
|
|||
[Fact] |
|||
public void Constructor_Should_Set_Value_Based_On_Initial_Entry() |
|||
{ |
|||
var target = new PriorityValue<string>( |
|||
Owner, |
|||
TestProperty, |
|||
ValueStore, |
|||
new ConstantValueEntry<string>( |
|||
TestProperty, |
|||
"1", |
|||
BindingPriority.StyleTrigger, |
|||
new(ValueStore))); |
|||
|
|||
Assert.Equal("1", target.GetValue().Value); |
|||
Assert.Equal(BindingPriority.StyleTrigger, target.Priority); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetValue_Should_Respect_MaxPriority() |
|||
{ |
|||
var target = new PriorityValue<string>( |
|||
Owner, |
|||
TestProperty, |
|||
ValueStore); |
|||
|
|||
target.SetValue("animation", BindingPriority.Animation); |
|||
target.SetValue("local", BindingPriority.LocalValue); |
|||
target.SetValue("styletrigger", BindingPriority.StyleTrigger); |
|||
target.SetValue("style", BindingPriority.Style); |
|||
|
|||
Assert.Equal("animation", target.GetValue(BindingPriority.Animation)); |
|||
Assert.Equal("local", target.GetValue(BindingPriority.LocalValue)); |
|||
Assert.Equal("styletrigger", target.GetValue(BindingPriority.StyleTrigger)); |
|||
Assert.Equal("style", target.GetValue(BindingPriority.TemplatedParent)); |
|||
Assert.Equal("style", target.GetValue(BindingPriority.Style)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_LocalValue_Should_Not_Add_Entries() |
|||
{ |
|||
var target = new PriorityValue<string>( |
|||
Owner, |
|||
TestProperty, |
|||
ValueStore); |
|||
|
|||
target.SetValue("1", BindingPriority.LocalValue); |
|||
target.SetValue("2", BindingPriority.LocalValue); |
|||
|
|||
Assert.Empty(target.Entries); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Non_LocalValue_Should_Add_Entries() |
|||
{ |
|||
var target = new PriorityValue<string>( |
|||
Owner, |
|||
TestProperty, |
|||
ValueStore); |
|||
|
|||
target.SetValue("1", BindingPriority.Style); |
|||
target.SetValue("2", BindingPriority.Animation); |
|||
|
|||
var result = target.Entries |
|||
.OfType<ConstantValueEntry<string>>() |
|||
.Select(x => x.GetValue().Value) |
|||
.ToList(); |
|||
|
|||
Assert.Equal(new[] { "1", "2" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Priority_Should_Be_Set() |
|||
{ |
|||
var target = new PriorityValue<string>( |
|||
Owner, |
|||
TestProperty, |
|||
ValueStore); |
|||
|
|||
Assert.Equal(BindingPriority.Unset, target.Priority); |
|||
target.SetValue("style", BindingPriority.Style); |
|||
Assert.Equal(BindingPriority.Style, target.Priority); |
|||
target.SetValue("local", BindingPriority.LocalValue); |
|||
Assert.Equal(BindingPriority.LocalValue, target.Priority); |
|||
target.SetValue("animation", BindingPriority.Animation); |
|||
Assert.Equal(BindingPriority.Animation, target.Priority); |
|||
target.SetValue("local2", BindingPriority.LocalValue); |
|||
Assert.Equal(BindingPriority.Animation, target.Priority); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_With_Same_Priority_Should_Be_Appended() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.LocalValue); |
|||
target.AddBinding(source2, BindingPriority.LocalValue); |
|||
|
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
Assert.Equal(new[] { "1", "2" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_With_Higher_Priority_Should_Be_Appended() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.LocalValue); |
|||
target.AddBinding(source2, BindingPriority.Animation); |
|||
|
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
Assert.Equal(new[] { "1", "2" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_With_Lower_Priority_Should_Be_Prepended() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.LocalValue); |
|||
target.AddBinding(source2, BindingPriority.Style); |
|||
|
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
Assert.Equal(new[] { "2", "1" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
var source3 = new Source("3"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.LocalValue); |
|||
target.AddBinding(source2, BindingPriority.Style); |
|||
target.AddBinding(source3, BindingPriority.Style); |
|||
|
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
Assert.Equal(new[] { "2", "3", "1" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Competed_Binding_Should_Be_Removed() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
var source3 = new Source("3"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.LocalValue).Start(); |
|||
target.AddBinding(source2, BindingPriority.Style).Start(); |
|||
target.AddBinding(source3, BindingPriority.Style).Start(); |
|||
source3.OnCompleted(); |
|||
|
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
Assert.Equal(new[] { "2", "1" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Value_Should_Come_From_Last_Entry() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
var source3 = new Source("3"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.LocalValue).Start(); |
|||
target.AddBinding(source2, BindingPriority.Style).Start(); |
|||
target.AddBinding(source3, BindingPriority.Style).Start(); |
|||
|
|||
Assert.Equal("1", target.GetValue().Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void LocalValue_Should_Override_LocalValue_Binding() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.LocalValue).Start(); |
|||
target.SetValue("2", BindingPriority.LocalValue); |
|||
|
|||
Assert.Equal("2", target.GetValue().Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void LocalValue_Should_Override_Style_Binding() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.Style).Start(); |
|||
target.SetValue("2", BindingPriority.LocalValue); |
|||
|
|||
Assert.Equal("2", target.GetValue().Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void LocalValue_Should_Not_Override_Animation_Binding() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.Animation).Start(); |
|||
target.SetValue("2", BindingPriority.LocalValue); |
|||
|
|||
Assert.Equal("1", target.GetValue().Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void NonAnimated_Value_Should_Be_Correct_1() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
var source3 = new Source("3"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.LocalValue).Start(); |
|||
target.AddBinding(source2, BindingPriority.Style).Start(); |
|||
target.AddBinding(source3, BindingPriority.Animation).Start(); |
|||
|
|||
Assert.Equal("3", target.GetValue().Value); |
|||
Assert.Equal("1", target.GetValue(BindingPriority.LocalValue).Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void NonAnimated_Value_Should_Be_Correct_2() |
|||
{ |
|||
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
var source3 = new Source("3"); |
|||
|
|||
target.AddBinding(source1, BindingPriority.Animation).Start(); |
|||
target.AddBinding(source2, BindingPriority.Style).Start(); |
|||
target.AddBinding(source3, BindingPriority.Style).Start(); |
|||
|
|||
Assert.Equal("1", target.GetValue().Value); |
|||
Assert.Equal("3", target.GetValue(BindingPriority.LocalValue).Value); |
|||
} |
|||
|
|||
private class Source : IObservable<BindingValue<string>> |
|||
{ |
|||
private IObserver<BindingValue<string>> _observer; |
|||
|
|||
public Source(string id) => Id = id; |
|||
public string Id { get; } |
|||
|
|||
public IDisposable Subscribe(IObserver<BindingValue<string>> observer) |
|||
{ |
|||
_observer = observer; |
|||
observer.OnNext(Id); |
|||
return Disposable.Empty; |
|||
} |
|||
|
|||
public void OnCompleted() => _observer.OnCompleted(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
using System.Collections.Generic; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
using Avalonia.PropertyStore; |
|||
using Avalonia.Styling; |
|||
using Microsoft.Reactive.Testing; |
|||
using Xunit; |
|||
using static Microsoft.Reactive.Testing.ReactiveTest; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Base.UnitTests.PropertyStore |
|||
{ |
|||
public class ValueStoreTests_Frames |
|||
{ |
|||
[Fact] |
|||
public void Adding_Frame_Raises_PropertyChanged() |
|||
{ |
|||
var target = new Class1(); |
|||
var subject = new BehaviorSubject<string>("bar"); |
|||
var result = new List<PropertyChange>(); |
|||
var style = new Style |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(Class1.FooProperty, "foo"), |
|||
new Setter(Class1.BarProperty, subject.ToBinding()), |
|||
} |
|||
}; |
|||
|
|||
target.PropertyChanged += (s, e) => |
|||
{ |
|||
result.Add(new(e.Property, e.OldValue, e.NewValue)); |
|||
}; |
|||
|
|||
var frame = InstanceStyle(style, target); |
|||
target.GetValueStore().AddFrame(frame); |
|||
|
|||
Assert.Equal(new PropertyChange[] |
|||
{ |
|||
new(Class1.FooProperty, "foodefault", "foo"), |
|||
new(Class1.BarProperty, "bardefault", "bar"), |
|||
}, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_Frame_Raises_PropertyChanged() |
|||
{ |
|||
var target = new Class1(); |
|||
var subject = new BehaviorSubject<string>("bar"); |
|||
var result = new List<PropertyChange>(); |
|||
var style = new Style |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(Class1.FooProperty, "foo"), |
|||
new Setter(Class1.BarProperty, subject.ToBinding()), |
|||
} |
|||
}; |
|||
var frame = InstanceStyle(style, target); |
|||
target.GetValueStore().AddFrame(frame); |
|||
|
|||
target.PropertyChanged += (s, e) => |
|||
{ |
|||
result.Add(new(e.Property, e.OldValue, e.NewValue)); |
|||
}; |
|||
|
|||
target.GetValueStore().RemoveFrame(frame); |
|||
|
|||
Assert.Equal(new PropertyChange[] |
|||
{ |
|||
new(Class1.BarProperty, "bar", "bardefault"), |
|||
new(Class1.FooProperty, "foo", "foodefault"), |
|||
}, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_Frame_Unsubscribes_Binding() |
|||
{ |
|||
var target = new Class1(); |
|||
var scheduler = new TestScheduler(); |
|||
var obs = scheduler.CreateColdObservable(OnNext(0, "bar")); |
|||
var style = new Style |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(Class1.FooProperty, "foo"), |
|||
new Setter(Class1.BarProperty, obs.ToBinding()), |
|||
} |
|||
}; |
|||
var frame = InstanceStyle(style, target); |
|||
|
|||
target.GetValueStore().AddFrame(frame); |
|||
target.GetValueStore().RemoveFrame(frame); |
|||
|
|||
Assert.Single(obs.Subscriptions); |
|||
Assert.Equal(0, obs.Subscriptions[0].Subscribe); |
|||
Assert.NotEqual(Subscription.Infinite, obs.Subscriptions[0].Unsubscribe); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Completing_Binding_Removes_ImmediateValueFrame() |
|||
{ |
|||
var target = new Class1(); |
|||
var source = new BehaviorSubject<BindingValue<string>>("foo"); |
|||
|
|||
target.Bind(Class1.FooProperty, source, BindingPriority.Animation); |
|||
|
|||
var valueStore = target.GetValueStore(); |
|||
Assert.Equal(1, valueStore.Frames.Count); |
|||
Assert.IsType<ImmediateValueFrame>(valueStore.Frames[0]); |
|||
|
|||
source.OnCompleted(); |
|||
|
|||
Assert.Equal(0, valueStore.Frames.Count); |
|||
} |
|||
|
|||
private static StyleInstance InstanceStyle(Style style, StyledElement target) |
|||
{ |
|||
var result = new StyleInstance(style, null); |
|||
|
|||
foreach (var setter in style.Setters) |
|||
result.Add(setter.Instance(result, target)); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private class Class1 : StyledElement |
|||
{ |
|||
public static readonly StyledProperty<string> FooProperty = |
|||
AvaloniaProperty.Register<Class1, string>("Foo", "foodefault"); |
|||
|
|||
public static readonly StyledProperty<string> BarProperty = |
|||
AvaloniaProperty.Register<Class1, string>("Bar", "bardefault", true); |
|||
} |
|||
|
|||
private record PropertyChange( |
|||
AvaloniaProperty Property, |
|||
object? OldValue, |
|||
object? NewValue); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue