Browse Source
Most (but not all) tests passing, all features mostly implemented exception coercion.refactor/style-priorities
84 changed files with 3809 additions and 3754 deletions
@ -0,0 +1,33 @@ |
|||||
|
#if !NET6_0_OR_GREATER
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
internal static class CollectionPolyfills |
||||
|
{ |
||||
|
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
|
||||
@ -1,154 +1,137 @@ |
|||||
using System; |
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Reactive.Disposables; |
||||
using Avalonia.Data; |
using Avalonia.Data; |
||||
using Avalonia.Threading; |
|
||||
|
|
||||
namespace Avalonia.PropertyStore |
namespace Avalonia.PropertyStore |
||||
{ |
{ |
||||
/// <summary>
|
internal class BindingEntry : IValueEntry, |
||||
/// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
|
IObserver<object?>, |
||||
/// </summary>
|
IDisposable |
||||
internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable |
|
||||
{ |
{ |
||||
void Start(bool ignoreBatchUpdate); |
private readonly ValueFrameBase _frame; |
||||
} |
private readonly IObservable<object?> _source; |
||||
|
|
||||
/// <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 IDisposable? _subscription; |
||||
private bool _isSubscribed; |
private bool _hasValue; |
||||
private bool _batchUpdate; |
private object? _value; |
||||
private Optional<T> _value; |
|
||||
|
|
||||
public BindingEntry( |
public BindingEntry( |
||||
AvaloniaObject owner, |
ValueFrameBase frame, |
||||
StyledPropertyBase<T> property, |
AvaloniaProperty property, |
||||
IObservable<BindingValue<T>> source, |
IObservable<object?> source) |
||||
BindingPriority priority, |
|
||||
ValueOwner<T> sink) |
|
||||
{ |
{ |
||||
_owner = owner; |
_frame = frame; |
||||
|
_source = source; |
||||
Property = property; |
Property = property; |
||||
Source = source; |
|
||||
Priority = priority; |
|
||||
_sink = sink; |
|
||||
} |
} |
||||
|
|
||||
public StyledPropertyBase<T> Property { get; } |
public bool HasValue |
||||
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; |
get |
||||
|
{ |
||||
if (_sink.IsValueStore) |
StartIfNecessary(); |
||||
Start(); |
return _hasValue; |
||||
|
} |
||||
} |
} |
||||
|
|
||||
public Optional<T> GetValue(BindingPriority maxPriority) |
public AvaloniaProperty Property { get; } |
||||
{ |
|
||||
return Priority >= maxPriority ? _value : Optional<T>.Empty; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
public void Dispose() |
||||
{ |
{ |
||||
_subscription?.Dispose(); |
Unsubscribe(); |
||||
_subscription = null; |
BindingCompleted(); |
||||
OnCompleted(); |
|
||||
} |
} |
||||
|
|
||||
public void OnCompleted() |
public object? GetValue() |
||||
{ |
{ |
||||
var oldValue = _value; |
StartIfNecessary(); |
||||
_value = default; |
if (!_hasValue) |
||||
Priority = BindingPriority.Unset; |
throw new AvaloniaInternalException("The binding entry has no value."); |
||||
_isSubscribed = false; |
return _value!; |
||||
_sink.Completed(Property, this, oldValue); |
|
||||
} |
} |
||||
|
|
||||
public void OnError(Exception error) |
public bool TryGetValue(out object? value) |
||||
{ |
{ |
||||
throw new NotImplementedException("BindingEntry.OnError is not implemented", error); |
StartIfNecessary(); |
||||
|
value = _value; |
||||
|
return _hasValue; |
||||
} |
} |
||||
|
|
||||
public void OnNext(BindingValue<T> value) |
public void Start() |
||||
{ |
{ |
||||
if (Dispatcher.UIThread.CheckAccess()) |
Debug.Assert(_subscription is null); |
||||
{ |
|
||||
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)); |
// Subscription won't be set until Subscribe completes, but in the meantime we
|
||||
} |
// need to signal that we've started as Subscribe may cause a value to be produced.
|
||||
|
_subscription = Disposable.Empty; |
||||
|
_subscription = _source.Subscribe(this); |
||||
} |
} |
||||
|
|
||||
public void Start() => Start(false); |
public void OnCompleted() => BindingCompleted(); |
||||
|
public void OnError(Exception error) => BindingCompleted(); |
||||
|
|
||||
|
public void OnNext(object? value) => SetValue(value); |
||||
|
|
||||
public void Start(bool ignoreBatchUpdate) |
public virtual void Unsubscribe() |
||||
{ |
{ |
||||
// We can't use _subscription to check whether we're subscribed because it won't be set
|
_subscription?.Dispose(); |
||||
// until Subscribe has finished, which will be too late to prevent reentrancy. In addition
|
_subscription = null; |
||||
// 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); |
private void ClearValue() |
||||
|
|
||||
public void RaiseValueChanged( |
|
||||
AvaloniaObject owner, |
|
||||
AvaloniaProperty property, |
|
||||
Optional<object?> oldValue, |
|
||||
Optional<object?> newValue) |
|
||||
{ |
{ |
||||
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( |
if (_hasValue) |
||||
owner, |
{ |
||||
(AvaloniaProperty<T>)property, |
_hasValue = false; |
||||
oldValue.Cast<T>(), |
_value = default; |
||||
newValue.Cast<T>(), |
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); |
||||
Priority)); |
} |
||||
} |
} |
||||
|
|
||||
private void UpdateValue(BindingValue<T> value) |
private void SetValue(object? value) |
||||
{ |
{ |
||||
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) |
if (_frame.Owner is null) |
||||
|
return; |
||||
|
|
||||
|
if (value is BindingNotification n) |
||||
{ |
{ |
||||
value = Property.GetDefaultValue(_owner.GetType()); |
value = n.Value; |
||||
} |
} |
||||
|
|
||||
if (value.Type == BindingValueType.DoNothing) |
if (value == AvaloniaProperty.UnsetValue) |
||||
{ |
{ |
||||
return; |
ClearValue(); |
||||
} |
} |
||||
|
else if (value == BindingOperations.DoNothing) |
||||
var old = _value; |
{ |
||||
|
// Do nothing!
|
||||
if (value.Type != BindingValueType.DataValidationError) |
} |
||||
|
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) |
||||
|
{ |
||||
|
if (!_hasValue || !Equals(_value, typedValue)) |
||||
|
{ |
||||
|
_value = typedValue; |
||||
|
_hasValue = true; |
||||
|
_frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, typedValue); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
{ |
{ |
||||
_value = value.ToOptional(); |
ClearValue(); |
||||
|
LoggingUtils.LogInvalidValue(_frame.Owner.Owner, Property, Property.PropertyType, value); |
||||
} |
} |
||||
|
} |
||||
|
|
||||
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(_owner, Property, old, value, Priority)); |
private void BindingCompleted() |
||||
|
{ |
||||
|
_subscription = null; |
||||
|
_frame.OnBindingCompleted(this); |
||||
|
} |
||||
|
|
||||
|
private void StartIfNecessary() |
||||
|
{ |
||||
|
if (_subscription is null) |
||||
|
Start(); |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,169 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Reactive.Disposables; |
||||
|
using Avalonia.Data; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal class BindingEntry<T> : IValueEntry<T>, |
||||
|
IObserver<T>, |
||||
|
IObserver<BindingValue<T>>, |
||||
|
IDisposable |
||||
|
{ |
||||
|
private readonly ValueFrameBase _frame; |
||||
|
private readonly object _source; |
||||
|
private IDisposable? _subscription; |
||||
|
private bool _hasValue; |
||||
|
private T? _value; |
||||
|
|
||||
|
public BindingEntry( |
||||
|
ValueFrameBase frame, |
||||
|
StyledPropertyBase<T> property, |
||||
|
IObservable<BindingValue<T>> source) |
||||
|
{ |
||||
|
_frame = frame; |
||||
|
_source = source; |
||||
|
Property = property; |
||||
|
} |
||||
|
|
||||
|
public BindingEntry( |
||||
|
ValueFrameBase frame, |
||||
|
StyledPropertyBase<T> property, |
||||
|
IObservable<T> source) |
||||
|
{ |
||||
|
_frame = frame; |
||||
|
_source = source; |
||||
|
Property = property; |
||||
|
} |
||||
|
|
||||
|
public bool HasValue |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
return _hasValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public StyledPropertyBase<T> Property { get; } |
||||
|
AvaloniaProperty IValueEntry.Property => Property; |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
Unsubscribe(); |
||||
|
BindingCompleted(); |
||||
|
} |
||||
|
|
||||
|
public T GetValue() |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
if (!_hasValue) |
||||
|
throw new AvaloniaInternalException("The binding entry has no value."); |
||||
|
return _value!; |
||||
|
} |
||||
|
|
||||
|
public void Start() |
||||
|
{ |
||||
|
Debug.Assert(_subscription is null); |
||||
|
|
||||
|
// Subscription won't be set until Subscribe completes, but in the meantime we
|
||||
|
// need to signal that we've started as Subscribe may cause a value to be produced.
|
||||
|
_subscription = Disposable.Empty; |
||||
|
|
||||
|
if (_source is IObservable<BindingValue<T>> bv) |
||||
|
_subscription = bv.Subscribe(this); |
||||
|
else if (_source is IObservable<T> b) |
||||
|
_subscription = b.Subscribe(this); |
||||
|
else |
||||
|
throw new AvaloniaInternalException("Unexpected binding source."); |
||||
|
} |
||||
|
|
||||
|
public bool TryGetValue([MaybeNullWhen(false)] out T value) |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
value = _value; |
||||
|
return _hasValue; |
||||
|
} |
||||
|
|
||||
|
public void OnCompleted() => BindingCompleted(); |
||||
|
public void OnError(Exception error) => BindingCompleted(); |
||||
|
|
||||
|
public void OnNext(T value) => SetValue(value); |
||||
|
|
||||
|
public void OnNext(BindingValue<T> value) |
||||
|
{ |
||||
|
if (_frame.Owner is not null) |
||||
|
LoggingUtils.LogIfNecessary(_frame.Owner.Owner, Property, value); |
||||
|
|
||||
|
if (value.HasValue) |
||||
|
SetValue(value.Value); |
||||
|
else |
||||
|
ClearValue(); |
||||
|
} |
||||
|
|
||||
|
public void Unsubscribe() |
||||
|
{ |
||||
|
_subscription?.Dispose(); |
||||
|
_subscription = null; |
||||
|
} |
||||
|
|
||||
|
object? IValueEntry.GetValue() |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
if (!_hasValue) |
||||
|
throw new AvaloniaInternalException("The BindingEntry<T> has no value."); |
||||
|
return _value!; |
||||
|
} |
||||
|
|
||||
|
bool IValueEntry.TryGetValue(out object? value) |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
value = _value; |
||||
|
return _hasValue; |
||||
|
} |
||||
|
|
||||
|
private void ClearValue() |
||||
|
{ |
||||
|
if (_hasValue) |
||||
|
{ |
||||
|
_hasValue = false; |
||||
|
_value = default; |
||||
|
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SetValue(T value) |
||||
|
{ |
||||
|
if (_frame.Owner is null) |
||||
|
return; |
||||
|
|
||||
|
if (Property.ValidateValue?.Invoke(value) != false) |
||||
|
{ |
||||
|
if (!_hasValue || !EqualityComparer<T>.Default.Equals(_value, value)) |
||||
|
{ |
||||
|
_value = value; |
||||
|
_hasValue = true; |
||||
|
_frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, value); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void BindingCompleted() |
||||
|
{ |
||||
|
_subscription = null; |
||||
|
_frame.OnBindingCompleted(this); |
||||
|
} |
||||
|
|
||||
|
private void StartIfNecessary() |
||||
|
{ |
||||
|
if (_subscription is null) |
||||
|
Start(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,25 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal static class DictionaryPool<TKey, TValue> |
||||
|
where TKey : notnull |
||||
|
{ |
||||
|
private const int MaxPoolSize = 4; |
||||
|
private static Stack<Dictionary<TKey, TValue>> _pool = new(); |
||||
|
|
||||
|
public static Dictionary<TKey, TValue> Get() |
||||
|
{ |
||||
|
return _pool.Count == 0 ? new() : _pool.Pop(); |
||||
|
} |
||||
|
|
||||
|
public static void Release(Dictionary<TKey, TValue> dictionary) |
||||
|
{ |
||||
|
if (_pool.Count < MaxPoolSize) |
||||
|
{ |
||||
|
dictionary.Clear(); |
||||
|
_pool.Push(dictionary); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
using System; |
||||
|
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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the current effective value as a boxed value.
|
||||
|
/// </summary>
|
||||
|
public object? Value => GetBoxedValue(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the current effective base value as a boxed value, or
|
||||
|
/// <see cref="AvaloniaProperty.UnsetValue"/> if not set.
|
||||
|
/// </summary>
|
||||
|
public object? BaseValue => GetBoxedBaseValue(); |
||||
|
|
||||
|
/// <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>
|
||||
|
/// Sets the value and 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>
|
||||
|
/// <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, |
||||
|
AvaloniaProperty property, |
||||
|
object? value, |
||||
|
BindingPriority priority); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the value and 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>
|
||||
|
/// <param name="value">The new value of the property.</param>
|
||||
|
/// <param name="priority">The priority of the new value.</param>
|
||||
|
/// <param name="baseValue">The new base value of the property.</param>
|
||||
|
/// <param name="basePriority">The priority of the new base value.</param>
|
||||
|
public abstract void SetAndRaise( |
||||
|
ValueStore owner, |
||||
|
AvaloniaProperty property, |
||||
|
object? value, |
||||
|
BindingPriority priority, |
||||
|
object? baseValue, |
||||
|
BindingPriority basePriority); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the value, raising <see cref="AvaloniaObject.PropertyChanged"/>
|
||||
|
/// where necessary.
|
||||
|
/// </summary>
|
||||
|
/// <param name="owner">The associated value store.</param>
|
||||
|
/// <param name="entry">The value entry with the new value of the property.</param>
|
||||
|
/// <param name="priority">The priority of the new value.</param>
|
||||
|
/// <remarks>
|
||||
|
/// This method does not set the base value.
|
||||
|
/// </remarks>
|
||||
|
public abstract void SetAndRaise( |
||||
|
ValueStore owner, |
||||
|
IValueEntry entry, |
||||
|
BindingPriority priority); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set the value priority, but leaves the value unchanged.
|
||||
|
/// </summary>
|
||||
|
public void SetPriority(BindingPriority priority) => Priority = BindingPriority.Unset; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set the base value priority, but leaves the base value unchanged.
|
||||
|
/// </summary>
|
||||
|
public void SetBasePriority(BindingPriority priority) => BasePriority = BindingPriority.Unset; |
||||
|
|
||||
|
/// <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>
|
||||
|
/// 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 abstract object? GetBoxedBaseValue(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,220 @@ |
|||||
|
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 T? _baseValue; |
||||
|
|
||||
|
public EffectiveValue(T value, BindingPriority priority) |
||||
|
{ |
||||
|
Value = value; |
||||
|
Priority = priority; |
||||
|
|
||||
|
if (priority >= BindingPriority.LocalValue && priority < BindingPriority.Inherited) |
||||
|
{ |
||||
|
_baseValue = value; |
||||
|
BasePriority = priority; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_baseValue = default; |
||||
|
BasePriority = BindingPriority.Unset; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the current effective value.
|
||||
|
/// </summary>
|
||||
|
public new T Value { get; private set; } |
||||
|
|
||||
|
public override void SetAndRaise( |
||||
|
ValueStore owner, |
||||
|
AvaloniaProperty property, |
||||
|
object? value, |
||||
|
BindingPriority priority) |
||||
|
{ |
||||
|
// `value` should already have been converted to the correct type and
|
||||
|
// validated by this point.
|
||||
|
SetAndRaise(owner, (StyledPropertyBase<T>)property, (T)value!, priority); |
||||
|
} |
||||
|
|
||||
|
public override void SetAndRaise( |
||||
|
ValueStore owner, |
||||
|
AvaloniaProperty property, |
||||
|
object? value, |
||||
|
BindingPriority priority, |
||||
|
object? baseValue, |
||||
|
BindingPriority basePriority) |
||||
|
{ |
||||
|
SetAndRaise(owner, (StyledPropertyBase<T>)property, (T)value!, priority, (T)baseValue!, basePriority); |
||||
|
} |
||||
|
|
||||
|
public override void SetAndRaise( |
||||
|
ValueStore owner, |
||||
|
IValueEntry entry, |
||||
|
BindingPriority priority) |
||||
|
{ |
||||
|
var value = entry is IValueEntry<T> typed ? typed.GetValue() : (T)entry.GetValue()!; |
||||
|
SetAndRaise(owner, (StyledPropertyBase<T>)entry.Property, value, priority); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the value and base value, raising <see cref="AvaloniaObject.PropertyChanged"/>
|
||||
|
/// where necessary.
|
||||
|
/// </summary>
|
||||
|
/// <param name="owner">The object on which to raise events.</param>
|
||||
|
/// <param name="property">The property being changed.</param>
|
||||
|
/// <param name="value">The new value of the property.</param>
|
||||
|
/// <param name="priority">The priority of the new value.</param>
|
||||
|
public void SetAndRaise( |
||||
|
ValueStore owner, |
||||
|
StyledPropertyBase<T> property, |
||||
|
T value, |
||||
|
BindingPriority priority) |
||||
|
{ |
||||
|
Debug.Assert(priority < BindingPriority.Inherited); |
||||
|
|
||||
|
var oldValue = Value; |
||||
|
var valueChanged = false; |
||||
|
var baseValueChanged = false; |
||||
|
|
||||
|
if (priority <= Priority) |
||||
|
{ |
||||
|
valueChanged = !EqualityComparer<T>.Default.Equals(Value, value); |
||||
|
Value = value; |
||||
|
Priority = priority; |
||||
|
} |
||||
|
|
||||
|
if (priority <= BasePriority && priority >= BindingPriority.LocalValue) |
||||
|
{ |
||||
|
baseValueChanged = !EqualityComparer<T>.Default.Equals(_baseValue, value); |
||||
|
_baseValue = value; |
||||
|
BasePriority = priority; |
||||
|
} |
||||
|
|
||||
|
if (valueChanged) |
||||
|
{ |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the value and base value, raising <see cref="AvaloniaObject.PropertyChanged"/>
|
||||
|
/// where necessary.
|
||||
|
/// </summary>
|
||||
|
/// <param name="owner">The object on which to raise events.</param>
|
||||
|
/// <param name="property">The property being changed.</param>
|
||||
|
/// <param name="value">The new value of the property.</param>
|
||||
|
/// <param name="priority">The priority of the new value.</param>
|
||||
|
/// <param name="baseValue">The new base value of the property.</param>
|
||||
|
/// <param name="basePriority">The priority of the new base value.</param>
|
||||
|
public void SetAndRaise( |
||||
|
ValueStore owner, |
||||
|
StyledPropertyBase<T> property, |
||||
|
T value, |
||||
|
BindingPriority priority, |
||||
|
T baseValue, |
||||
|
BindingPriority basePriority) |
||||
|
{ |
||||
|
Debug.Assert(priority < BindingPriority.Inherited); |
||||
|
Debug.Assert(basePriority > BindingPriority.Animation); |
||||
|
|
||||
|
var oldValue = Value; |
||||
|
var valueChanged = false; |
||||
|
var baseValueChanged = false; |
||||
|
|
||||
|
if (!EqualityComparer<T>.Default.Equals(Value, value)) |
||||
|
{ |
||||
|
Value = value; |
||||
|
valueChanged = true; |
||||
|
} |
||||
|
|
||||
|
if (BasePriority == BindingPriority.Unset || |
||||
|
!EqualityComparer<T>.Default.Equals(_baseValue, baseValue)) |
||||
|
{ |
||||
|
_baseValue = value; |
||||
|
baseValueChanged = true; |
||||
|
} |
||||
|
|
||||
|
Priority = priority; |
||||
|
BasePriority = basePriority; |
||||
|
|
||||
|
if (valueChanged) |
||||
|
{ |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 : p.GetDefaultValue(owner.GetType()); |
||||
|
var n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : p.GetDefaultValue(owner.GetType()); |
||||
|
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 DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) |
||||
|
{ |
||||
|
DisposeAndRaiseUnset(owner, (StyledPropertyBase<T>)property); |
||||
|
} |
||||
|
|
||||
|
public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase<T> property) |
||||
|
{ |
||||
|
var defaultValue = property.GetDefaultValue(owner.GetType()); |
||||
|
|
||||
|
if (!EqualityComparer<T>.Default.Equals(defaultValue, Value)) |
||||
|
{ |
||||
|
owner.Owner.RaisePropertyChanged(property, Value, defaultValue, BindingPriority.Unset, true); |
||||
|
if (property.Inherits) |
||||
|
owner.OnInheritedEffectiveValueDisposed(property, Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override object? GetBoxedValue() => Value; |
||||
|
|
||||
|
protected override object? GetBoxedBaseValue() |
||||
|
{ |
||||
|
return BasePriority != BindingPriority.Unset ? _baseValue : AvaloniaProperty.UnsetValue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,42 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents an untyped value entry in an <see cref="IValueFrame"/>.
|
||||
|
/// </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>
|
||||
|
/// Tries to get the value associated with the entry.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value">
|
||||
|
/// When this method returns, contains the value associated with the entry if a value is
|
||||
|
/// present; otherwise, returns null.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// true if the entry has an associated value; otherwise false.
|
||||
|
/// </returns>
|
||||
|
bool TryGetValue(out object? value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the value entry is removed from the value store.
|
||||
|
/// </summary>
|
||||
|
void Unsubscribe(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a typed value entry in an <see cref="IValueFrame"/>.
|
||||
|
/// </summary>
|
||||
|
internal interface IValueEntry<T> : IValueEntry |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the property that this value applies to.
|
||||
|
/// </summary>
|
||||
|
new StyledPropertyBase<T> Property { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the value associated with the entry.
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AvaloniaInternalException">
|
||||
|
/// The entry has no value.
|
||||
|
/// </exception>
|
||||
|
new T GetValue(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tries to get the value associated with the entry.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value">
|
||||
|
/// When this method returns, contains the value associated with the entry if a value is
|
||||
|
/// present; otherwise, returns the default value of <typeparamref name="T"/>.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// true if the entry has an associated value; otherwise false.
|
||||
|
/// </returns>
|
||||
|
bool TryGetValue(out T? value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using Avalonia.Data; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a collection of property values in a <see cref="PropertyStore.ValueStore"/>.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// A value frame is an abstraction over the following sources of values in an
|
||||
|
/// <see cref="AvaloniaObject"/>:
|
||||
|
///
|
||||
|
/// - A style
|
||||
|
/// - Local values
|
||||
|
/// - Animation values
|
||||
|
/// </remarks>
|
||||
|
internal interface IValueFrame : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the number of value entries in the frame.
|
||||
|
/// </summary>
|
||||
|
int EntryCount { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether the frame is active.
|
||||
|
/// </summary>
|
||||
|
bool IsActive { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the value store that owns the frame.
|
||||
|
/// </summary>
|
||||
|
ValueStore? Owner { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the frame's priority.
|
||||
|
/// </summary>
|
||||
|
BindingPriority Priority { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retreives the frame's value entry with the specified index.
|
||||
|
/// </summary>
|
||||
|
IValueEntry GetEntry(int index); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the owner of the value frame.
|
||||
|
/// </summary>
|
||||
|
/// <param name="owner">The new owner.</param>
|
||||
|
void SetOwner(ValueStore? owner); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tries to retreive the frame's value entry for the specified property.
|
||||
|
/// </summary>
|
||||
|
bool TryGetEntry(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? entry); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
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 T GetValue() => _value; |
||||
|
|
||||
|
public bool TryGetValue(out T? value) |
||||
|
{ |
||||
|
value = _value; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public bool TryGetValue(out object? value) |
||||
|
{ |
||||
|
value = _value; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public void Unsubscribe() { } |
||||
|
|
||||
|
public void Dispose() => _owner.OnEntryDisposed(this); |
||||
|
|
||||
|
object? IValueEntry.GetValue() => _value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
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 : ValueFrameBase |
||||
|
{ |
||||
|
public ImmediateValueFrame(BindingPriority priority) |
||||
|
{ |
||||
|
Priority = priority; |
||||
|
} |
||||
|
|
||||
|
public override bool IsActive => true; |
||||
|
public override BindingPriority Priority { get; } |
||||
|
|
||||
|
public BindingEntry<T> AddBinding<T>( |
||||
|
StyledPropertyBase<T> property, |
||||
|
IObservable<BindingValue<T>> source) |
||||
|
{ |
||||
|
var e = new BindingEntry<T>(this, property, source); |
||||
|
Add(e); |
||||
|
return e; |
||||
|
} |
||||
|
|
||||
|
public BindingEntry<T> AddBinding<T>( |
||||
|
StyledPropertyBase<T> property, |
||||
|
IObservable<T> source) |
||||
|
{ |
||||
|
var e = new BindingEntry<T>(this, property, source); |
||||
|
Add(e); |
||||
|
return e; |
||||
|
} |
||||
|
|
||||
|
public UntypedBindingEntry<T> AddBinding<T>( |
||||
|
StyledPropertyBase<T> property, |
||||
|
IObservable<object?> source) |
||||
|
{ |
||||
|
var e = new UntypedBindingEntry<T>(this, property, source); |
||||
|
Add(e); |
||||
|
return e; |
||||
|
} |
||||
|
|
||||
|
public IDisposable 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal class InheritanceFrame : Dictionary<AvaloniaProperty, EffectiveValue> |
||||
|
{ |
||||
|
public InheritanceFrame(ValueStore owner, InheritanceFrame? parent = null) |
||||
|
{ |
||||
|
Owner = owner; |
||||
|
Parent = parent; |
||||
|
} |
||||
|
|
||||
|
public ValueStore Owner { get; } |
||||
|
public InheritanceFrame? Parent { get; private set; } |
||||
|
|
||||
|
public bool TryGetFromThisOrAncestor(AvaloniaProperty property, [NotNullWhen(true)] out EffectiveValue? value) |
||||
|
{ |
||||
|
var frame = this; |
||||
|
|
||||
|
while (frame is object) |
||||
|
{ |
||||
|
if (frame.TryGetValue(property, out value)) |
||||
|
return true; |
||||
|
frame = frame.Parent; |
||||
|
} |
||||
|
|
||||
|
value = default; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public void SetParent(InheritanceFrame? value) => Parent = value; |
||||
|
} |
||||
|
} |
||||
@ -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,61 @@ |
|||||
|
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; |
||||
|
} |
||||
|
|
||||
|
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,61 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Logging; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal static class LoggingUtils |
||||
|
{ |
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void LogIfNecessary<T>( |
||||
|
AvaloniaObject owner, |
||||
|
AvaloniaProperty property, |
||||
|
BindingValue<T> value) |
||||
|
{ |
||||
|
if (value.HasError) |
||||
|
Log(owner, property, value); |
||||
|
} |
||||
|
|
||||
|
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<T>( |
||||
|
AvaloniaObject owner, |
||||
|
AvaloniaProperty property, |
||||
|
BindingValue<T> value) |
||||
|
{ |
||||
|
owner.GetBindingWarningLogger(property, value.Error)?.Log( |
||||
|
owner, |
||||
|
"Error in binding to {Target}.{Property}: {Message}", |
||||
|
owner, |
||||
|
property, |
||||
|
value.Error!.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,163 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Reactive.Disposables; |
||||
|
using Avalonia.Data; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal class UntypedBindingEntry<T> : IValueEntry<T>, |
||||
|
IObserver<object?>, |
||||
|
IDisposable |
||||
|
{ |
||||
|
private readonly ValueFrameBase _frame; |
||||
|
private readonly IObservable<object?> _source; |
||||
|
private IDisposable? _subscription; |
||||
|
private bool _hasValue; |
||||
|
private T? _value; |
||||
|
|
||||
|
public UntypedBindingEntry( |
||||
|
ValueFrameBase frame, |
||||
|
StyledPropertyBase<T> property, |
||||
|
IObservable<object?> source) |
||||
|
{ |
||||
|
_frame = frame; |
||||
|
_source = source; |
||||
|
Property = property; |
||||
|
} |
||||
|
|
||||
|
public bool HasValue |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
return _hasValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public StyledPropertyBase<T> Property { get; } |
||||
|
AvaloniaProperty IValueEntry.Property => Property; |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
Unsubscribe(); |
||||
|
BindingCompleted(); |
||||
|
} |
||||
|
|
||||
|
public T GetValue() |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
if (!_hasValue) |
||||
|
throw new AvaloniaInternalException("The binding entry has no value."); |
||||
|
return _value!; |
||||
|
} |
||||
|
|
||||
|
public void Start() |
||||
|
{ |
||||
|
Debug.Assert(_subscription is null); |
||||
|
|
||||
|
// Subscription won't be set until Subscribe completes, but in the meantime we
|
||||
|
// need to signal that we've started as Subscribe may cause a value to be produced.
|
||||
|
_subscription = Disposable.Empty; |
||||
|
_subscription = _source.Subscribe(this); |
||||
|
} |
||||
|
|
||||
|
public bool TryGetValue([MaybeNullWhen(false)] out T value) |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
value = _value; |
||||
|
return _hasValue; |
||||
|
} |
||||
|
|
||||
|
public void OnCompleted() => BindingCompleted(); |
||||
|
public void OnError(Exception error) => BindingCompleted(); |
||||
|
|
||||
|
public void OnNext(object? value) => SetValue(value); |
||||
|
|
||||
|
public void OnNext(BindingValue<T> value) |
||||
|
{ |
||||
|
if (value.HasValue) |
||||
|
SetValue(value.Value); |
||||
|
else |
||||
|
ClearValue(); |
||||
|
} |
||||
|
|
||||
|
public void Unsubscribe() |
||||
|
{ |
||||
|
_subscription?.Dispose(); |
||||
|
_subscription = null; |
||||
|
} |
||||
|
|
||||
|
object? IValueEntry.GetValue() |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
if (!_hasValue) |
||||
|
throw new AvaloniaInternalException("The BindingEntry<T> has no value."); |
||||
|
return _value!; |
||||
|
} |
||||
|
|
||||
|
bool IValueEntry.TryGetValue(out object? value) |
||||
|
{ |
||||
|
StartIfNecessary(); |
||||
|
value = _value; |
||||
|
return _hasValue; |
||||
|
} |
||||
|
|
||||
|
private void ClearValue() |
||||
|
{ |
||||
|
if (_hasValue) |
||||
|
{ |
||||
|
_hasValue = false; |
||||
|
_value = default; |
||||
|
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SetValue(object? value) |
||||
|
{ |
||||
|
if (_frame.Owner is null) |
||||
|
return; |
||||
|
|
||||
|
if (value is BindingNotification n) |
||||
|
{ |
||||
|
value = n.Value; |
||||
|
} |
||||
|
|
||||
|
if (value == AvaloniaProperty.UnsetValue) |
||||
|
{ |
||||
|
ClearValue(); |
||||
|
} |
||||
|
else if (value == BindingOperations.DoNothing) |
||||
|
{ |
||||
|
// Do nothing!
|
||||
|
} |
||||
|
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) |
||||
|
{ |
||||
|
if (!_hasValue || !EqualityComparer<T>.Default.Equals(_value, typedValue)) |
||||
|
{ |
||||
|
_value = typedValue; |
||||
|
_hasValue = true; |
||||
|
_frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, typedValue); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
ClearValue(); |
||||
|
LoggingUtils.LogInvalidValue(_frame.Owner.Owner, Property, typeof(T), value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void BindingCompleted() |
||||
|
{ |
||||
|
_subscription = null; |
||||
|
_frame.OnBindingCompleted(this); |
||||
|
} |
||||
|
|
||||
|
private void StartIfNecessary() |
||||
|
{ |
||||
|
if (_subscription is null) |
||||
|
Start(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal static class UntypedValueUtils |
||||
|
{ |
||||
|
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,54 @@ |
|||||
|
using System.Diagnostics; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal abstract class ValueFrameBase : IValueFrame |
||||
|
{ |
||||
|
private readonly AvaloniaPropertyValueStore<IValueEntry> _entries = new(); |
||||
|
|
||||
|
public int EntryCount => _entries.Count; |
||||
|
public abstract bool IsActive { get; } |
||||
|
public ValueStore? Owner { get; private set; } |
||||
|
public abstract BindingPriority Priority { get; } |
||||
|
|
||||
|
public bool Contains(AvaloniaProperty property) => _entries.Contains(property); |
||||
|
|
||||
|
public IValueEntry GetEntry(int index) => _entries[index]; |
||||
|
|
||||
|
public void SetOwner(ValueStore? owner) => Owner = owner; |
||||
|
|
||||
|
public bool TryGet(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? value) |
||||
|
{ |
||||
|
return _entries.TryGetValue(property, out value); |
||||
|
} |
||||
|
|
||||
|
public bool TryGetEntry(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? entry) |
||||
|
{ |
||||
|
return _entries.TryGetValue(property, out entry); |
||||
|
} |
||||
|
|
||||
|
public void OnBindingCompleted(IValueEntry binding) |
||||
|
{ |
||||
|
Remove(binding.Property); |
||||
|
Owner?.OnBindingCompleted(binding.Property, this); |
||||
|
} |
||||
|
|
||||
|
public virtual void Dispose() |
||||
|
{ |
||||
|
for (var i = 0; i < _entries.Count; ++i) |
||||
|
_entries[i].Unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
protected void Add(IValueEntry value) |
||||
|
{ |
||||
|
Debug.Assert(!value.Property.IsDirect); |
||||
|
_entries.AddValue(value.Property, value); |
||||
|
} |
||||
|
|
||||
|
protected void Remove(AvaloniaProperty property) => _entries.Remove(property); |
||||
|
protected void Set(IValueEntry value) => _entries.SetValue(value.Property, value); |
||||
|
} |
||||
|
} |
||||
@ -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,948 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using Avalonia.Collections.Pooled; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Diagnostics; |
||||
|
using Avalonia.Logging; |
||||
|
|
||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal class ValueStore |
||||
|
{ |
||||
|
private readonly List<IValueFrame> _frames = new(); |
||||
|
private Dictionary<int, IDisposable>? _localValueBindings; |
||||
|
private InheritanceFrame? _inheritanceFrame; |
||||
|
private Dictionary<AvaloniaProperty, EffectiveValue>? _effectiveValues; |
||||
|
private int _frameGeneration; |
||||
|
private int _styling; |
||||
|
|
||||
|
public ValueStore(AvaloniaObject owner) => Owner = owner; |
||||
|
|
||||
|
public AvaloniaObject Owner { get; } |
||||
|
public IReadOnlyList<IValueFrame> Frames => _frames; |
||||
|
|
||||
|
public void BeginStyling() => ++_styling; |
||||
|
|
||||
|
public void EndStyling() |
||||
|
{ |
||||
|
if (--_styling == 0) |
||||
|
ReevaluateEffectiveValues(); |
||||
|
} |
||||
|
|
||||
|
public void AddFrame(IValueFrame 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); |
||||
|
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); |
||||
|
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); |
||||
|
var result = frame.AddBinding(property, source); |
||||
|
|
||||
|
if (effective is null || priority <= effective.Priority) |
||||
|
result.Start(); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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}."); |
||||
|
} |
||||
|
|
||||
|
IDisposable? result = null; |
||||
|
|
||||
|
if (priority != BindingPriority.LocalValue) |
||||
|
{ |
||||
|
var frame = GetOrCreateImmediateValueFrame(property, priority); |
||||
|
result = frame.AddValue(property, value); |
||||
|
InsertFrame(frame); |
||||
|
} |
||||
|
|
||||
|
if (TryGetEffectiveValue(property, out var existing)) |
||||
|
{ |
||||
|
var effective = (EffectiveValue<T>)existing; |
||||
|
effective.SetAndRaise(this, property, value, priority); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
AddEffectiveValueAndRaise(property, value, priority); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public object? GetValue(AvaloniaProperty property) |
||||
|
{ |
||||
|
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v)) |
||||
|
return v.Value; |
||||
|
if (_inheritanceFrame is not null && _inheritanceFrame.TryGetFromThisOrAncestor(property, out v)) |
||||
|
return v.Value; |
||||
|
|
||||
|
return GetDefaultValue(property); |
||||
|
} |
||||
|
|
||||
|
public T GetValue<T>(StyledPropertyBase<T> property) |
||||
|
{ |
||||
|
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v)) |
||||
|
return ((EffectiveValue<T>)v).Value; |
||||
|
if (_inheritanceFrame is not null && _inheritanceFrame.TryGetFromThisOrAncestor(property, out v)) |
||||
|
return ((EffectiveValue<T>)v).Value; |
||||
|
return property.GetDefaultValue(Owner.GetType()); |
||||
|
} |
||||
|
|
||||
|
public bool IsAnimating(AvaloniaProperty property) |
||||
|
{ |
||||
|
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v)) |
||||
|
return v.Priority <= BindingPriority.Animation; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public bool IsSet(AvaloniaProperty property) |
||||
|
{ |
||||
|
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v)) |
||||
|
return v.Priority < BindingPriority.Inherited; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
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 void SetInheritanceParent(AvaloniaObject? oldParent, AvaloniaObject? newParent) |
||||
|
{ |
||||
|
var values = DictionaryPool<AvaloniaProperty, OldNewValue>.Get(); |
||||
|
var oldInheritanceFrame = oldParent?.GetValueStore()._inheritanceFrame; |
||||
|
var newInheritanceFrame = newParent?.GetValueStore().OnBecameInheritanceParent(); |
||||
|
|
||||
|
// The old and new parents are the same, nothing to do here.
|
||||
|
if (oldInheritanceFrame == newInheritanceFrame) |
||||
|
return; |
||||
|
|
||||
|
// First get the old values from the old inheritance parent.
|
||||
|
var f = oldInheritanceFrame; |
||||
|
|
||||
|
while (f is not null) |
||||
|
{ |
||||
|
foreach (var i in f) |
||||
|
{ |
||||
|
values.TryAdd(i.Key, new(i.Value)); |
||||
|
} |
||||
|
f = f.Parent; |
||||
|
} |
||||
|
|
||||
|
f = newInheritanceFrame; |
||||
|
|
||||
|
// Get the new values from the new inheritance parent.
|
||||
|
while (f is not null) |
||||
|
{ |
||||
|
foreach (var i in f) |
||||
|
{ |
||||
|
if (values.TryGetValue(i.Key, out var existing)) |
||||
|
values[i.Key] = existing.WithNewValue(i.Value); |
||||
|
else |
||||
|
values.Add(i.Key, new(null, i.Value)); |
||||
|
} |
||||
|
f = f.Parent; |
||||
|
} |
||||
|
|
||||
|
ParentInheritanceFrameChanged(newInheritanceFrame); |
||||
|
|
||||
|
// Raise PropertyChanged events where necessary on this object and inheritance children.
|
||||
|
foreach (var i in values) |
||||
|
{ |
||||
|
var oldValue = i.Value.OldValue; |
||||
|
var newValue = i.Value.NewValue; |
||||
|
|
||||
|
if (oldValue != newValue) |
||||
|
InheritedValueChanged(i.Key, oldValue, newValue); |
||||
|
} |
||||
|
|
||||
|
DictionaryPool<AvaloniaProperty, OldNewValue>.Release(values); |
||||
|
} |
||||
|
|
||||
|
public void FrameActivationChanged(IValueFrame frame) |
||||
|
{ |
||||
|
ReevaluateEffectiveValues(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called by an inheritance child to notify the value store that it has become an
|
||||
|
/// inheritance parent. Creates and returns an inheritance frame if necessary.
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
public InheritanceFrame? OnBecameInheritanceParent() |
||||
|
{ |
||||
|
if (_inheritanceFrame is not null) |
||||
|
return _inheritanceFrame; |
||||
|
if (_effectiveValues is null) |
||||
|
return null; |
||||
|
|
||||
|
foreach (var i in _effectiveValues) |
||||
|
{ |
||||
|
if (i.Key.Inherits) |
||||
|
return GetOrCreateInheritanceFrame(true); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
|
||||
|
/// binding produces a new value.
|
||||
|
/// </summary>
|
||||
|
/// <param name="property">The bound property.</param>
|
||||
|
/// <param name="priority">The priority of binding which produced a new value.</param>
|
||||
|
/// <param name="value">The new value.</param>
|
||||
|
public void OnBindingValueChanged( |
||||
|
AvaloniaProperty property, |
||||
|
BindingPriority priority, |
||||
|
object? value) |
||||
|
{ |
||||
|
Debug.Assert(priority != BindingPriority.LocalValue); |
||||
|
|
||||
|
if (TryGetEffectiveValue(property, out var existing)) |
||||
|
{ |
||||
|
if (priority <= existing.Priority) |
||||
|
ReevaluateEffectiveValue(property, existing); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
AddEffectiveValueAndRaise(property, value, priority); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
|
||||
|
/// binding produces a new value.
|
||||
|
/// </summary>
|
||||
|
/// <param name="property">The bound property.</param>
|
||||
|
/// <param name="priority">The priority of binding which produced a new value.</param>
|
||||
|
/// <param name="value">The new value.</param>
|
||||
|
public void OnBindingValueChanged<T>( |
||||
|
StyledPropertyBase<T> property, |
||||
|
BindingPriority priority, |
||||
|
T value) |
||||
|
{ |
||||
|
Debug.Assert(priority != BindingPriority.LocalValue); |
||||
|
|
||||
|
if (TryGetEffectiveValue(property, out var existing)) |
||||
|
{ |
||||
|
if (priority <= existing.Priority) |
||||
|
ReevaluateEffectiveValue(property, existing); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
AddEffectiveValueAndRaise(property, value, 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="BindingEntry{T}"/> to re-evaluate the effective value when the
|
||||
|
/// binding completes or terminates on error.
|
||||
|
/// </summary>
|
||||
|
/// <param name="property">The previously bound property.</param>
|
||||
|
/// <param name="frame">The frame which contained the binding.</param>
|
||||
|
public void OnBindingCompleted(AvaloniaProperty property, IValueFrame frame) |
||||
|
{ |
||||
|
var priority = frame.Priority; |
||||
|
|
||||
|
if (TryGetEffectiveValue(property, out var existing)) |
||||
|
{ |
||||
|
if (priority <= existing.Priority) |
||||
|
ReevaluateEffectiveValue(property, existing); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <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 we have children or an existing inheritance frame, then make sure it's owned and
|
||||
|
// set the value. If we have no children and no inheritance frame then it will be
|
||||
|
// created when it's needed.
|
||||
|
if (children is not null || _inheritanceFrame is not null) |
||||
|
GetOrCreateInheritanceFrame(true)[property] = value; |
||||
|
|
||||
|
if (children is not null) |
||||
|
{ |
||||
|
var count = children.Count; |
||||
|
|
||||
|
for (var i = 0; i < count; ++i) |
||||
|
{ |
||||
|
children[i].GetValueStore().OnParentInheritedValueChanged(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); |
||||
|
Debug.Assert(_inheritanceFrame is null || _inheritanceFrame.Owner == this); |
||||
|
|
||||
|
if (_inheritanceFrame is null || _inheritanceFrame.Owner != this) |
||||
|
return; |
||||
|
|
||||
|
_inheritanceFrame.Remove(property); |
||||
|
|
||||
|
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().OnParentInheritedValueChanged(property, oldValue, defaultValue); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when a <see cref="LocalValueBindingObserver{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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void OnParentInheritedValueChanged<T>( |
||||
|
StyledPropertyBase<T> property, |
||||
|
T oldValue, |
||||
|
T newValue) |
||||
|
{ |
||||
|
Debug.Assert(property.Inherits); |
||||
|
|
||||
|
// Ensure the inheritance frame is created.
|
||||
|
GetOrCreateInheritanceFrame(false); |
||||
|
|
||||
|
// If the inherited value is set locally, propagation stops here.
|
||||
|
if (_effectiveValues is not null && _effectiveValues.ContainsKey(property)) |
||||
|
return; |
||||
|
|
||||
|
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().OnParentInheritedValueChanged(property, oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called by an <see cref="IValueFrame"/> 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(IValueFrame frame, AvaloniaProperty property) |
||||
|
{ |
||||
|
Debug.Assert(frame.IsActive); |
||||
|
|
||||
|
if (TryGetEffectiveValue(property, out var existing)) |
||||
|
{ |
||||
|
if (frame.Priority <= existing.Priority) |
||||
|
ReevaluateEffectiveValue(property, existing); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Error, LogArea.Property)?.Log( |
||||
|
Owner, |
||||
|
"Internal error: ValueStore.OnEntryRemoved called for {Property} " + |
||||
|
"but no effective value was found.", |
||||
|
property); |
||||
|
Debug.Assert(false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool RemoveFrame(IValueFrame frame) |
||||
|
{ |
||||
|
if (_frames.Remove(frame)) |
||||
|
{ |
||||
|
frame.Dispose(); |
||||
|
++_frameGeneration; |
||||
|
ReevaluateEffectiveValues(); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property) |
||||
|
{ |
||||
|
var effective = GetEffectiveValue(property); |
||||
|
return new AvaloniaPropertyValue( |
||||
|
property, |
||||
|
effective?.Value, |
||||
|
effective?.Priority ?? BindingPriority.Unset, |
||||
|
null); |
||||
|
} |
||||
|
|
||||
|
private void InsertFrame(IValueFrame frame) |
||||
|
{ |
||||
|
var index = _frames.BinarySearch(frame, FrameInsertionComparer.Instance); |
||||
|
if (index < 0) |
||||
|
index = ~index; |
||||
|
_frames.Insert(index, frame); |
||||
|
++_frameGeneration; |
||||
|
frame.SetOwner(this); |
||||
|
} |
||||
|
|
||||
|
private InheritanceFrame GetOrCreateInheritanceFrame(bool owned) |
||||
|
{ |
||||
|
if (_inheritanceFrame is null) |
||||
|
{ |
||||
|
var parentFrame = Owner.InheritanceParent?.GetValueStore()._inheritanceFrame; |
||||
|
|
||||
|
_inheritanceFrame = owned || parentFrame is null ? |
||||
|
new(this, parentFrame) : |
||||
|
parentFrame; |
||||
|
|
||||
|
if (_effectiveValues is not null) |
||||
|
{ |
||||
|
foreach (var i in _effectiveValues) |
||||
|
{ |
||||
|
if (i.Key.Inherits) |
||||
|
_inheritanceFrame[i.Key] = i.Value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else if (owned && _inheritanceFrame.Owner != this) |
||||
|
{ |
||||
|
_inheritanceFrame = new(this, _inheritanceFrame); |
||||
|
} |
||||
|
|
||||
|
return _inheritanceFrame; |
||||
|
} |
||||
|
|
||||
|
private ImmediateValueFrame GetOrCreateImmediateValueFrame( |
||||
|
AvaloniaProperty property, |
||||
|
BindingPriority priority) |
||||
|
{ |
||||
|
Debug.Assert(priority != BindingPriority.LocalValue); |
||||
|
|
||||
|
// TODO: Binary search?
|
||||
|
for (var i = _frames.Count - 1; i >= 0; --i) |
||||
|
{ |
||||
|
var frame = _frames[i]; |
||||
|
if (frame is ImmediateValueFrame immediate && !immediate.Contains(property)) |
||||
|
return immediate; |
||||
|
if (frame.Priority > priority) |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
var result = new ImmediateValueFrame(priority); |
||||
|
InsertFrame(result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private void ReevaluateEffectiveValue( |
||||
|
AvaloniaProperty property, |
||||
|
EffectiveValue current, |
||||
|
bool ignoreLocalValue = false) |
||||
|
{ |
||||
|
if (EvaluateEffectiveValue( |
||||
|
property, |
||||
|
!ignoreLocalValue ? current : null, |
||||
|
out var value, |
||||
|
out var priority, |
||||
|
out var baseValue, |
||||
|
out var basePriority)) |
||||
|
{ |
||||
|
if (basePriority != BindingPriority.Unset) |
||||
|
current.SetAndRaise(this, property, value, priority, baseValue, basePriority); |
||||
|
else |
||||
|
current.SetAndRaise(this, property, value, priority); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_effectiveValues?.Remove(property); |
||||
|
current.DisposeAndRaiseUnset(this, property); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
|
||||
|
/// event and notifies inheritance children if necessary .
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The property type.</typeparam>
|
||||
|
/// <param name="property">The property.</param>
|
||||
|
/// <param name="value">The property value.</param>
|
||||
|
/// <param name="priority">The value priority.</param>
|
||||
|
private void AddEffectiveValueAndRaise(AvaloniaProperty property, object? value, BindingPriority priority) |
||||
|
{ |
||||
|
Debug.Assert(priority < BindingPriority.Inherited); |
||||
|
var effectiveValue = property.CreateEffectiveValue(Owner); |
||||
|
_effectiveValues ??= new(); |
||||
|
_effectiveValues.Add(property, effectiveValue); |
||||
|
effectiveValue.SetAndRaise(this, property, value, priority); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
|
||||
|
/// event and notifies inheritance children if necessary .
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The property type.</typeparam>
|
||||
|
/// <param name="property">The property.</param>
|
||||
|
/// <param name="value">The property value.</param>
|
||||
|
/// <param name="priority">The value priority.</param>
|
||||
|
private void AddEffectiveValueAndRaise<T>(StyledPropertyBase<T> property, T value, BindingPriority priority) |
||||
|
{ |
||||
|
Debug.Assert(priority < BindingPriority.Inherited); |
||||
|
var defaultValue = property.GetDefaultValue(Owner.GetType()); |
||||
|
var effectiveValue = new EffectiveValue<T>(defaultValue, BindingPriority.Unset); |
||||
|
_effectiveValues ??= new(); |
||||
|
_effectiveValues.Add(property, effectiveValue); |
||||
|
effectiveValue.SetAndRaise(this, property, value, priority); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Evaluates the current value and base value for a property based on the current frames and optionally
|
||||
|
/// local values. Does not evaluate inherited values.
|
||||
|
/// </summary>
|
||||
|
/// <param name="property">The property to evaluation</param>
|
||||
|
/// <param name="current">The current effective value if the local value is to be considered.</param>
|
||||
|
/// <param name="value">When the method exits will contain the current value if it exists.</param>
|
||||
|
/// <param name="priority">When the method exits will contain the current value priority.</param>
|
||||
|
/// <param name="baseValue">>When the method exits will contain the current base value if it exists.</param>
|
||||
|
/// <param name="basePriority">When the method exits will contain the current base value priority.</param>
|
||||
|
/// <returns>
|
||||
|
/// True if a value was found, otherwise false.
|
||||
|
/// </returns>
|
||||
|
private bool EvaluateEffectiveValue( |
||||
|
AvaloniaProperty property, |
||||
|
EffectiveValue? current, |
||||
|
out object? value, |
||||
|
out BindingPriority priority, |
||||
|
out object? baseValue, |
||||
|
out BindingPriority basePriority) |
||||
|
{ |
||||
|
var i = _frames.Count - 1; |
||||
|
|
||||
|
value = baseValue = AvaloniaProperty.UnsetValue; |
||||
|
priority = basePriority = BindingPriority.Unset; |
||||
|
|
||||
|
// First try to find an animation value.
|
||||
|
for (; i >= 0; --i) |
||||
|
{ |
||||
|
var frame = _frames[i]; |
||||
|
|
||||
|
if (frame.Priority > BindingPriority.Animation) |
||||
|
break; |
||||
|
|
||||
|
if (frame.IsActive && |
||||
|
frame.TryGetEntry(property, out var entry) && |
||||
|
entry.TryGetValue(out value)) |
||||
|
{ |
||||
|
priority = frame.Priority; |
||||
|
--i; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Local values come from the current EffectiveValue.
|
||||
|
if (current?.Priority == BindingPriority.LocalValue) |
||||
|
{ |
||||
|
// If there's a current effective local value and no animated value then we use the
|
||||
|
// effective local value.
|
||||
|
if (priority == BindingPriority.Unset) |
||||
|
{ |
||||
|
value = current.Value; |
||||
|
priority = BindingPriority.LocalValue; |
||||
|
} |
||||
|
|
||||
|
// The local value is always the base value.
|
||||
|
baseValue = current.Value; |
||||
|
basePriority = BindingPriority.LocalValue; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Or the current effective base value if there's no longer an animated value.
|
||||
|
if (priority == BindingPriority.Unset && current?.BasePriority == BindingPriority.LocalValue) |
||||
|
{ |
||||
|
value = baseValue = current.BaseValue; |
||||
|
priority = basePriority = BindingPriority.LocalValue; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Now try the rest of the frames.
|
||||
|
for (; i >= 0; --i) |
||||
|
{ |
||||
|
var frame = _frames[i]; |
||||
|
|
||||
|
if (frame.IsActive && |
||||
|
frame.TryGetEntry(property, out var entry) && |
||||
|
entry.TryGetValue(out var v)) |
||||
|
{ |
||||
|
if (priority == BindingPriority.Unset) |
||||
|
{ |
||||
|
value = v; |
||||
|
priority = frame.Priority; |
||||
|
} |
||||
|
|
||||
|
baseValue = v; |
||||
|
basePriority = frame.Priority; |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return priority != BindingPriority.Unset; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
// 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 ParentInheritanceFrameChanged(InheritanceFrame? parent) |
||||
|
{ |
||||
|
if (_inheritanceFrame?.Owner == this) |
||||
|
{ |
||||
|
_inheritanceFrame.SetParent(parent); |
||||
|
} |
||||
|
else if (_inheritanceFrame != parent) |
||||
|
{ |
||||
|
_inheritanceFrame = parent; |
||||
|
|
||||
|
var children = Owner.GetInheritanceChildren(); |
||||
|
|
||||
|
if (children is null) |
||||
|
return; |
||||
|
|
||||
|
var count = children.Count; |
||||
|
|
||||
|
for (var i = 0; i < count; ++i) |
||||
|
{ |
||||
|
children[i].GetValueStore().ParentInheritanceFrameChanged(parent); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void ReevaluateEffectiveValues() |
||||
|
{ |
||||
|
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; |
||||
|
|
||||
|
// Reset all non-LocalValue effective values to Unset priority.
|
||||
|
if (_effectiveValues is not null) |
||||
|
{ |
||||
|
foreach (var v in _effectiveValues) |
||||
|
{ |
||||
|
var e = v.Value; |
||||
|
|
||||
|
if (e.Priority != BindingPriority.LocalValue) |
||||
|
e.SetPriority(BindingPriority.Unset); |
||||
|
if (e.BasePriority != BindingPriority.LocalValue) |
||||
|
e.SetBasePriority(BindingPriority.Unset); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 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; |
||||
|
var count = frame.EntryCount; |
||||
|
|
||||
|
for (var j = 0; j < count; ++j) |
||||
|
{ |
||||
|
var entry = frame.GetEntry(j); |
||||
|
|
||||
|
if (!entry.HasValue) |
||||
|
continue; |
||||
|
|
||||
|
var property = entry.Property; |
||||
|
|
||||
|
if (_effectiveValues is not null && |
||||
|
_effectiveValues.TryGetValue(property, out var effectiveValue)) |
||||
|
{ |
||||
|
if (effectiveValue.Priority == BindingPriority.Unset || |
||||
|
effectiveValue.BasePriority == BindingPriority.Unset) |
||||
|
{ |
||||
|
effectiveValue.SetAndRaise(this, entry, priority); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var v = property.CreateEffectiveValue(Owner); |
||||
|
_effectiveValues ??= new(); |
||||
|
_effectiveValues.Add(property, v); |
||||
|
v.SetAndRaise(this, entry, priority); |
||||
|
} |
||||
|
|
||||
|
if (generation != _frameGeneration) |
||||
|
goto restart; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Remove all effective values that are still unset.
|
||||
|
if (_effectiveValues is not null) |
||||
|
{ |
||||
|
PooledList<AvaloniaProperty>? remove = null; |
||||
|
|
||||
|
foreach (var v in _effectiveValues) |
||||
|
{ |
||||
|
var e = v.Value; |
||||
|
|
||||
|
if (e.Priority == BindingPriority.Unset) |
||||
|
{ |
||||
|
remove ??= new(); |
||||
|
remove.Add(v.Key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (remove is not null) |
||||
|
{ |
||||
|
foreach (var v in remove) |
||||
|
{ |
||||
|
if (_effectiveValues.Remove(v, out var e)) |
||||
|
e.DisposeAndRaiseUnset(this, v); |
||||
|
} |
||||
|
remove.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[MemberNotNullWhen(true, nameof(_effectiveValues))] |
||||
|
private bool TryGetEffectiveValue( |
||||
|
AvaloniaProperty property, |
||||
|
[NotNullWhen(true)] out EffectiveValue? value) |
||||
|
{ |
||||
|
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out value)) |
||||
|
return true; |
||||
|
value = null; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property) |
||||
|
{ |
||||
|
if (_effectiveValues is not null && _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 class FrameInsertionComparer : IComparer<IValueFrame> |
||||
|
{ |
||||
|
public static readonly FrameInsertionComparer Instance = new FrameInsertionComparer(); |
||||
|
public int Compare(IValueFrame? x, IValueFrame? y) |
||||
|
{ |
||||
|
var result = y!.Priority - x!.Priority; |
||||
|
return result != 0 ? result : -1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
namespace Avalonia.Styling |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Represents a setter that has been instanced on a control.
|
/// Represents an <see cref="ISetter"/> that has been instanced on a control.
|
||||
/// </summary>
|
/// </summary>
|
||||
[Unstable] |
[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,41 @@ |
|||||
using System; |
using System; |
||||
using System.Reactive.Subjects; |
using System.Reactive.Linq; |
||||
using Avalonia.Data; |
using Avalonia.Data; |
||||
using Avalonia.Reactive; |
using Avalonia.PropertyStore; |
||||
|
|
||||
#nullable enable |
|
||||
|
|
||||
namespace Avalonia.Styling |
namespace Avalonia.Styling |
||||
{ |
{ |
||||
/// <summary>
|
internal class PropertySetterBindingInstance : BindingEntry, ISetterInstance |
||||
/// 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 |
|
||||
{ |
{ |
||||
private readonly IStyleable _target; |
private readonly IDisposable? _twoWaySubscription; |
||||
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; |
|
||||
|
|
||||
public PropertySetterBindingInstance( |
|
||||
IStyleable target, |
|
||||
StyledPropertyBase<T> property, |
|
||||
IBinding binding) |
|
||||
{ |
|
||||
_target = target; |
|
||||
_styledProperty = property; |
|
||||
_binding = binding.Initiate(_target, property); |
|
||||
|
|
||||
if (_binding?.Mode == BindingMode.OneTime) |
|
||||
{ |
|
||||
// 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."); |
|
||||
} |
|
||||
|
|
||||
_inner = new Inner(this); |
|
||||
} |
|
||||
|
|
||||
public PropertySetterBindingInstance( |
public PropertySetterBindingInstance( |
||||
IStyleable target, |
AvaloniaObject target, |
||||
DirectPropertyBase<T> property, |
StyleInstance instance, |
||||
IBinding binding) |
AvaloniaProperty property, |
||||
{ |
BindingMode mode, |
||||
_target = target; |
IObservable<object?> source) |
||||
_directProperty = property; |
: base(instance, property, source) |
||||
_binding = binding.Initiate(_target, property); |
|
||||
_inner = new Inner(this); |
|
||||
} |
|
||||
|
|
||||
public void Start(bool hasActivator) |
|
||||
{ |
|
||||
if (_binding is null) |
|
||||
return; |
|
||||
|
|
||||
_isActive = !hasActivator; |
|
||||
|
|
||||
if (_styledProperty is object) |
|
||||
{ |
|
||||
if (_binding.Mode != BindingMode.OneWayToSource) |
|
||||
{ |
|
||||
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; |
|
||||
_subscription = _target.Bind(_styledProperty, this, priority); |
|
||||
} |
|
||||
|
|
||||
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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Activate() |
|
||||
{ |
|
||||
if (_binding is null) |
|
||||
return; |
|
||||
|
|
||||
if (!_isActive) |
|
||||
{ |
|
||||
_innerSubscription ??= _binding.Observable!.Subscribe(_inner); |
|
||||
_isActive = true; |
|
||||
PublishNext(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Deactivate() |
|
||||
{ |
{ |
||||
if (_isActive) |
if (mode == BindingMode.TwoWay) |
||||
{ |
{ |
||||
_isActive = false; |
// TODO: HUGE HACK FIXME
|
||||
_innerSubscription?.Dispose(); |
if (source is IObserver<object?> observer) |
||||
_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); |
_twoWaySubscription = target.GetObservable(property).Skip(1).Subscribe(observer); |
||||
} |
} |
||||
else |
else |
||||
{ |
{ |
||||
PublishNext(); |
throw new NotSupportedException( |
||||
|
"Attempting to bind two-way with a binding source which doesn't support it."); |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
protected override void Unsubscribed() |
public override void Unsubscribe() |
||||
{ |
|
||||
_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; |
_twoWaySubscription?.Dispose(); |
||||
public Inner(PropertySetterBindingInstance<T> owner) => _owner = owner; |
base.Unsubscribe(); |
||||
public void OnCompleted() => _owner.PublishCompleted(); |
|
||||
public void OnError(Exception error) => _owner.PublishError(error); |
|
||||
public void OnNext(object? value) => _owner.ConvertAndPublishNext(value); |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,127 +1,34 @@ |
|||||
using System; |
using System; |
||||
using Avalonia.Data; |
using Avalonia.PropertyStore; |
||||
using Avalonia.Reactive; |
|
||||
|
|
||||
#nullable enable |
|
||||
|
|
||||
namespace Avalonia.Styling |
namespace Avalonia.Styling |
||||
{ |
{ |
||||
/// <summary>
|
internal class PropertySetterTemplateInstance : IValueEntry, ISetterInstance |
||||
/// 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 |
|
||||
{ |
{ |
||||
private readonly IStyleable _target; |
|
||||
private readonly StyledPropertyBase<T>? _styledProperty; |
|
||||
private readonly DirectPropertyBase<T>? _directProperty; |
|
||||
private readonly ITemplate _template; |
private readonly ITemplate _template; |
||||
private BindingValue<T> _value; |
private object? _value; |
||||
private IDisposable? _subscription; |
|
||||
private bool _isActive; |
|
||||
|
|
||||
public PropertySetterTemplateInstance( |
|
||||
IStyleable target, |
|
||||
StyledPropertyBase<T> property, |
|
||||
ITemplate template) |
|
||||
{ |
|
||||
_target = target; |
|
||||
_styledProperty = property; |
|
||||
_template = template; |
|
||||
} |
|
||||
|
|
||||
public PropertySetterTemplateInstance( |
public PropertySetterTemplateInstance(AvaloniaProperty property, ITemplate template) |
||||
IStyleable target, |
|
||||
DirectPropertyBase<T> property, |
|
||||
ITemplate template) |
|
||||
{ |
{ |
||||
_target = target; |
|
||||
_directProperty = property; |
|
||||
_template = template; |
_template = template; |
||||
|
Property = property; |
||||
} |
} |
||||
|
|
||||
public void Start(bool hasActivator) |
public bool HasValue => true; |
||||
{ |
public AvaloniaProperty Property { get; } |
||||
_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() |
public object? GetValue() |
||||
{ |
{ |
||||
if (!_isActive) |
TryGetValue(out var value); |
||||
{ |
return value; |
||||
_isActive = true; |
|
||||
PublishNext(); |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
public void Deactivate() |
public bool TryGetValue(out object? value) |
||||
{ |
{ |
||||
if (_isActive) |
value = _value ??= _template.Build(); |
||||
{ |
return value != AvaloniaProperty.UnsetValue; |
||||
_isActive = false; |
|
||||
PublishNext(); |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
public override void Dispose() |
void IValueEntry.Unsubscribe() { } |
||||
{ |
|
||||
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(); |
|
||||
} |
|
||||
|
|
||||
protected override void Subscribed() => PublishNext(); |
|
||||
protected override void Unsubscribed() { } |
|
||||
|
|
||||
private void EnsureTemplate() |
|
||||
{ |
|
||||
if (_value.HasValue) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
_value = (T) _template.Build(); |
|
||||
} |
|
||||
|
|
||||
private void PublishNext() |
|
||||
{ |
|
||||
if (_isActive) |
|
||||
{ |
|
||||
EnsureTemplate(); |
|
||||
PublishNext(_value); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
PublishNext(default); |
|
||||
} |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,137 +1,74 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using System.Reactive.Subjects; |
using Avalonia.Data; |
||||
using Avalonia.Animation; |
using Avalonia.PropertyStore; |
||||
using Avalonia.Styling.Activators; |
using Avalonia.Styling.Activators; |
||||
|
|
||||
#nullable enable |
|
||||
|
|
||||
namespace Avalonia.Styling |
namespace Avalonia.Styling |
||||
{ |
{ |
||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||
internal sealed class StyleInstance : IStyleInstance, IStyleActivatorSink |
/// <remarks>
|
||||
|
/// <see cref="StyleInstance"/> implements the <see cref="IValueFrame"/> interface 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 : ValueFrameBase, IStyleInstance, IStyleActivatorSink, IDisposable |
||||
{ |
{ |
||||
private readonly ISetterInstance[]? _setters; |
|
||||
private readonly IDisposable[]? _animations; |
|
||||
private readonly IStyleActivator? _activator; |
private readonly IStyleActivator? _activator; |
||||
private readonly Subject<bool>? _animationTrigger; |
private List<ISetterInstance>? _setters; |
||||
|
private bool _isActivatorInitializing; |
||||
|
private bool _isActivatorSubscribed; |
||||
|
|
||||
public StyleInstance( |
public StyleInstance(IStyle style, IStyleActivator? activator) |
||||
IStyle source, |
|
||||
IStyleable target, |
|
||||
IReadOnlyList<ISetter>? setters, |
|
||||
IReadOnlyList<IAnimation>? animations, |
|
||||
IStyleActivator? activator = null) |
|
||||
{ |
{ |
||||
Source = source ?? throw new ArgumentNullException(nameof(source)); |
|
||||
Target = target ?? throw new ArgumentNullException(nameof(target)); |
|
||||
_activator = activator; |
_activator = activator; |
||||
IsActive = _activator is null; |
Priority = activator is object ? BindingPriority.StyleTrigger : BindingPriority.Style; |
||||
|
Source = style; |
||||
if (setters is not null) |
} |
||||
{ |
|
||||
var setterCount = setters.Count; |
|
||||
|
|
||||
_setters = new ISetterInstance[setterCount]; |
|
||||
|
|
||||
for (var i = 0; i < setterCount; ++i) |
public bool HasActivator => _activator is object; |
||||
{ |
|
||||
_setters[i] = setters[i].Instance(Target); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (animations is not null && target is Animatable animatable) |
public override bool IsActive |
||||
|
{ |
||||
|
get |
||||
{ |
{ |
||||
var animationsCount = animations.Count; |
if (_activator is object && !_isActivatorSubscribed) |
||||
|
|
||||
_animations = new IDisposable[animationsCount]; |
|
||||
_animationTrigger = new Subject<bool>(); |
|
||||
|
|
||||
for (var i = 0; i < animationsCount; ++i) |
|
||||
{ |
{ |
||||
_animations[i] = animations[i].Apply(animatable, null, _animationTrigger); |
_isActivatorInitializing = true; |
||||
|
_activator.Subscribe(this); |
||||
|
_isActivatorInitializing = false; |
||||
|
_isActivatorSubscribed = true; |
||||
} |
} |
||||
|
|
||||
|
return _activator?.IsActive ?? true; |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
public bool HasActivator => _activator is not null; |
public override BindingPriority Priority { get; } |
||||
public bool IsActive { get; private set; } |
|
||||
public IStyle Source { get; } |
public IStyle Source { get; } |
||||
public IStyleable Target { get; } |
|
||||
|
|
||||
public void Start() |
public void Add(ISetterInstance instance) |
||||
{ |
{ |
||||
var hasActivator = HasActivator; |
if (instance is IValueEntry valueEntry) |
||||
|
base.Add(valueEntry); |
||||
if (_setters is not null) |
else |
||||
{ |
(_setters ??= new()).Add(instance); |
||||
foreach (var setter in _setters) |
|
||||
{ |
|
||||
setter.Start(hasActivator); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (hasActivator) |
|
||||
{ |
|
||||
_activator!.Subscribe(this, 0); |
|
||||
} |
|
||||
else if (_animationTrigger is not null) |
|
||||
{ |
|
||||
_animationTrigger.OnNext(true); |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
public void Dispose() |
public override void Dispose() |
||||
{ |
{ |
||||
if (_setters is not null) |
base.Dispose(); |
||||
{ |
|
||||
foreach (var setter in _setters) |
|
||||
{ |
|
||||
setter.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (_animations is not null) |
|
||||
{ |
|
||||
foreach (var subscription in _animations) |
|
||||
{ |
|
||||
subscription.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
_activator?.Dispose(); |
_activator?.Dispose(); |
||||
} |
} |
||||
|
|
||||
private void ActivatorChanged(bool value) |
void IStyleActivatorSink.OnNext(bool value, int tag) |
||||
{ |
{ |
||||
if (IsActive != value) |
if (!_isActivatorInitializing) |
||||
{ |
Owner?.FrameActivationChanged(this); |
||||
IsActive = value; |
|
||||
|
|
||||
_animationTrigger?.OnNext(value); |
|
||||
|
|
||||
if (_setters is not null) |
|
||||
{ |
|
||||
if (IsActive) |
|
||||
{ |
|
||||
foreach (var setter in _setters) |
|
||||
{ |
|
||||
setter.Activate(); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
foreach (var setter in _setters) |
|
||||
{ |
|
||||
setter.Deactivate(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(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,126 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Reactive; |
||||
|
using System.Reactive.Subjects; |
||||
|
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.FooProperty, "foo", "foodefault"), |
||||
|
new(Class1.BarProperty, "bar", "bardefault"), |
||||
|
}, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Removing_Frame_Unsubscribes_Binding() |
||||
|
{ |
||||
|
var target = new Class1(); |
||||
|
var scheduler = new TestScheduler(); |
||||
|
var obs = scheduler.CreateColdObservable(OnNext(0, "bar")); |
||||
|
var result = new List<PropertyChange>(); |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -1,147 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
using System.Runtime.CompilerServices; |
|
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Controls.Primitives; |
|
||||
using Avalonia.Controls.Templates; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Styling; |
|
||||
using BenchmarkDotNet.Attributes; |
|
||||
|
|
||||
#nullable enable |
|
||||
|
|
||||
namespace Avalonia.Benchmarks.Styling |
|
||||
{ |
|
||||
[MemoryDiagnoser] |
|
||||
public class ControlTheme_Apply |
|
||||
{ |
|
||||
private ControlTheme _theme; |
|
||||
private ControlTheme _otherTheme; |
|
||||
private List<Style> _styles = new(); |
|
||||
|
|
||||
public ControlTheme_Apply() |
|
||||
{ |
|
||||
RuntimeHelpers.RunClassConstructor(typeof(TestControl).TypeHandle); |
|
||||
|
|
||||
_theme = CreateControlTheme(Brushes.Red); |
|
||||
_otherTheme = CreateControlTheme(Brushes.Orange); |
|
||||
|
|
||||
for (var i = 0; i < 100; ++i) |
|
||||
{ |
|
||||
_styles.Add(new Style(x => x.OfType<TestControl>()) |
|
||||
{ |
|
||||
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Yellow) } |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Benchmark] |
|
||||
public void Apply_Control_Theme() |
|
||||
{ |
|
||||
var target = new TestControl(); |
|
||||
|
|
||||
target.BeginBatchUpdate(); |
|
||||
|
|
||||
_theme.TryAttach(target, null); |
|
||||
target.ApplyTemplate(); |
|
||||
_theme.TryAttach(target.VisualChild, null); |
|
||||
|
|
||||
target.EndBatchUpdate(); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
[Benchmark] |
|
||||
public void Apply_Remove_Control_Theme() |
|
||||
{ |
|
||||
var target = new TestControl(); |
|
||||
|
|
||||
target.BeginBatchUpdate(); |
|
||||
|
|
||||
_theme.TryAttach(target, null); |
|
||||
target.ApplyTemplate(); |
|
||||
_theme.TryAttach(target.VisualChild, null); |
|
||||
|
|
||||
target.EndBatchUpdate(); |
|
||||
|
|
||||
// Switching to another theme will cause the current theme to be removed but won't
|
|
||||
// immediately apply the new theme, so for the benefit of the benchmark it has the
|
|
||||
// effect of simply removing the theme.
|
|
||||
target.Theme = _otherTheme; |
|
||||
} |
|
||||
|
|
||||
[Benchmark] |
|
||||
public void Apply_Control_Theme_With_Styles() |
|
||||
{ |
|
||||
var target = new TestControl(); |
|
||||
|
|
||||
target.BeginBatchUpdate(); |
|
||||
|
|
||||
_theme.TryAttach(target, null); |
|
||||
target.ApplyTemplate(); |
|
||||
_theme.TryAttach(target.VisualChild, null); |
|
||||
|
|
||||
foreach (var style in _styles) |
|
||||
style.TryAttach(target, null); |
|
||||
|
|
||||
target.EndBatchUpdate(); |
|
||||
} |
|
||||
|
|
||||
[Benchmark] |
|
||||
public void Apply_Remove_Control_Theme_With_Styles() |
|
||||
{ |
|
||||
var target = new TestControl(); |
|
||||
|
|
||||
target.BeginBatchUpdate(); |
|
||||
|
|
||||
_theme.TryAttach(target, null); |
|
||||
target.ApplyTemplate(); |
|
||||
_theme.TryAttach(target.VisualChild, null); |
|
||||
|
|
||||
foreach (var style in _styles) |
|
||||
style.TryAttach(target, null); |
|
||||
|
|
||||
target.EndBatchUpdate(); |
|
||||
|
|
||||
// Switching to another theme will cause the current theme to be removed but won't
|
|
||||
// immediately apply the new theme, so for the benefit of the benchmark it has the
|
|
||||
// effect of simply removing the theme.
|
|
||||
target.Theme = _otherTheme; |
|
||||
} |
|
||||
|
|
||||
private static ControlTheme CreateControlTheme(IBrush background) |
|
||||
{ |
|
||||
return new ControlTheme(typeof(TestControl)) |
|
||||
{ |
|
||||
Setters = |
|
||||
{ |
|
||||
new Setter(TestControl.BackgroundProperty, Brushes.Transparent), |
|
||||
new Setter(TestControl.TemplateProperty, new FuncControlTemplate<TestControl>((_, x) => |
|
||||
new Border())), |
|
||||
}, |
|
||||
Children = |
|
||||
{ |
|
||||
new Style(x => x.Nesting().Template().OfType<Border>()) |
|
||||
{ |
|
||||
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Red), } |
|
||||
}, |
|
||||
new Style(x => x.Nesting().Class(":pointerover").Template().OfType<Border>()) |
|
||||
{ |
|
||||
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Green), } |
|
||||
}, |
|
||||
new Style(x => x.Nesting().Class(":pressed").Template().OfType<Border>()) |
|
||||
{ |
|
||||
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Blue), } |
|
||||
}, |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
private class TestControl : TemplatedControl |
|
||||
{ |
|
||||
public IStyleable VisualChild => (IStyleable)VisualChildren[0]; |
|
||||
} |
|
||||
|
|
||||
private class TestClass2 : Control |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue