csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1293 lines
45 KiB
1293 lines
45 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using Avalonia.Data;
|
|
using Avalonia.Data.Core;
|
|
using Avalonia.Diagnostics;
|
|
using Avalonia.Styling;
|
|
using Avalonia.Threading;
|
|
using Avalonia.Utilities;
|
|
|
|
namespace Avalonia.PropertyStore
|
|
{
|
|
internal class ValueStore : IBindingExpressionSink
|
|
{
|
|
private readonly List<ValueFrame> _frames = new();
|
|
private Dictionary<int, IDisposable>? _localValueBindings;
|
|
private AvaloniaPropertyDictionary<EffectiveValue> _effectiveValues;
|
|
private int _inheritedValueCount;
|
|
private int _isEvaluating;
|
|
private int _frameGeneration;
|
|
private int _styling;
|
|
|
|
public ValueStore(AvaloniaObject owner) => Owner = owner;
|
|
|
|
public AvaloniaObject Owner { get; }
|
|
public ValueStore? InheritanceAncestor { get; private set; }
|
|
public bool IsEvaluating => _isEvaluating > 0;
|
|
public IReadOnlyList<ValueFrame> Frames => _frames;
|
|
|
|
public void BeginStyling() => ++_styling;
|
|
|
|
public void EndStyling()
|
|
{
|
|
if (--_styling == 0)
|
|
ReevaluateEffectiveValues();
|
|
}
|
|
|
|
public void AddFrame(ValueFrame style)
|
|
{
|
|
InsertFrame(style);
|
|
ReevaluateEffectiveValues();
|
|
}
|
|
|
|
public BindingExpressionBase AddBinding(
|
|
AvaloniaProperty property,
|
|
UntypedBindingExpressionBase source)
|
|
{
|
|
var priority = source.Priority;
|
|
if (priority == BindingPriority.LocalValue || property.IsDirect)
|
|
{
|
|
DisposeExistingLocalValueBinding(property);
|
|
_localValueBindings ??= new();
|
|
_localValueBindings[property.Id] = source;
|
|
source.AttachAndStart(this, Owner, property, BindingPriority.LocalValue);
|
|
return source;
|
|
}
|
|
else
|
|
{
|
|
var effective = GetEffectiveValue(property);
|
|
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
|
|
|
|
source.Attach(this, frame, Owner, property, priority);
|
|
frame.AddBinding(source);
|
|
|
|
if (effective is null || priority <= effective.Priority)
|
|
source.Start();
|
|
|
|
return source;
|
|
}
|
|
}
|
|
|
|
public IDisposable AddBinding<T>(
|
|
StyledProperty<T> property,
|
|
IObservable<BindingValue<T>> source,
|
|
BindingPriority priority)
|
|
{
|
|
if (priority == BindingPriority.LocalValue)
|
|
{
|
|
var observer = new LocalValueBindingObserver<T>(this, property);
|
|
DisposeExistingLocalValueBinding(property);
|
|
_localValueBindings ??= new();
|
|
_localValueBindings[property.Id] = observer;
|
|
observer.Start(source);
|
|
return observer;
|
|
}
|
|
else
|
|
{
|
|
var effective = GetEffectiveValue(property);
|
|
|
|
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
|
|
var result = frame.AddBinding(property, source);
|
|
|
|
if (effective is null || priority <= effective.Priority)
|
|
result.Start();
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public IDisposable AddBinding<T>(
|
|
StyledProperty<T> property,
|
|
IObservable<T> source,
|
|
BindingPriority priority)
|
|
{
|
|
if (priority == BindingPriority.LocalValue)
|
|
{
|
|
var observer = new LocalValueBindingObserver<T>(this, property);
|
|
DisposeExistingLocalValueBinding(property);
|
|
_localValueBindings ??= new();
|
|
_localValueBindings[property.Id] = observer;
|
|
observer.Start(source);
|
|
return observer;
|
|
}
|
|
else
|
|
{
|
|
var effective = GetEffectiveValue(property);
|
|
|
|
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
|
|
var result = frame.AddBinding(property, source);
|
|
|
|
if (effective is null || priority <= effective.Priority)
|
|
result.Start();
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public IDisposable AddBinding<T>(
|
|
StyledProperty<T> property,
|
|
IObservable<object?> source,
|
|
BindingPriority priority)
|
|
{
|
|
if (priority == BindingPriority.LocalValue)
|
|
{
|
|
var observer = new LocalValueBindingObserver<T>(this, property);
|
|
DisposeExistingLocalValueBinding(property);
|
|
_localValueBindings ??= new();
|
|
_localValueBindings[property.Id] = observer;
|
|
observer.Start(source);
|
|
return observer;
|
|
}
|
|
else
|
|
{
|
|
var effective = GetEffectiveValue(property);
|
|
|
|
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
|
|
var result = frame.AddBinding(property, source);
|
|
|
|
if (effective is null || priority <= effective.Priority)
|
|
result.Start();
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<BindingValue<T>> source)
|
|
{
|
|
var observer = new DirectBindingObserver<T>(this, property);
|
|
DisposeExistingLocalValueBinding(property);
|
|
_localValueBindings ??= new();
|
|
_localValueBindings[property.Id] = observer;
|
|
observer.Start(source);
|
|
return observer;
|
|
}
|
|
|
|
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<T> source)
|
|
{
|
|
var observer = new DirectBindingObserver<T>(this, property);
|
|
DisposeExistingLocalValueBinding(property);
|
|
_localValueBindings ??= new();
|
|
_localValueBindings[property.Id] = observer;
|
|
observer.Start(source);
|
|
return observer;
|
|
}
|
|
|
|
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<object?> source)
|
|
{
|
|
var observer = new DirectUntypedBindingObserver<T>(this, property);
|
|
DisposeExistingLocalValueBinding(property);
|
|
_localValueBindings ??= new();
|
|
_localValueBindings[property.Id] = observer;
|
|
observer.Start(source);
|
|
return observer;
|
|
}
|
|
|
|
public void ClearValue(AvaloniaProperty property)
|
|
{
|
|
if (TryGetEffectiveValue(property, out var effective) &&
|
|
(effective.Priority == BindingPriority.LocalValue || effective.IsOverridenCurrentValue))
|
|
{
|
|
effective.IsOverridenCurrentValue = false;
|
|
ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true);
|
|
}
|
|
}
|
|
|
|
public IDisposable? SetValue<T>(StyledProperty<T> property, T value, BindingPriority priority)
|
|
{
|
|
if (property.ValidateValue?.Invoke(value) == false)
|
|
{
|
|
throw new ArgumentException($"{value} is not a valid value for '{property.Name}'.");
|
|
}
|
|
|
|
if (priority != BindingPriority.LocalValue)
|
|
{
|
|
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
|
|
var result = frame.AddValue(property, value);
|
|
|
|
if (TryGetEffectiveValue(property, out var existing))
|
|
{
|
|
var effective = (EffectiveValue<T>)existing;
|
|
effective.SetAndRaise(this, result, priority);
|
|
}
|
|
else
|
|
{
|
|
var effectiveValue = CreateEffectiveValue(property);
|
|
AddEffectiveValue(property, effectiveValue);
|
|
effectiveValue.SetAndRaise(this, result, priority);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
else
|
|
{
|
|
SetLocalValue(property, value);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void SetCurrentValue<T>(StyledProperty<T> property, T value)
|
|
{
|
|
if (TryGetEffectiveValue(property, out var v))
|
|
{
|
|
((EffectiveValue<T>)v).SetCurrentValueAndRaise(this, property, value);
|
|
}
|
|
else
|
|
{
|
|
var effectiveValue = CreateEffectiveValue(property);
|
|
AddEffectiveValue(property, effectiveValue);
|
|
effectiveValue.SetCurrentValueAndRaise(this, property, value);
|
|
}
|
|
}
|
|
|
|
public void SetLocalValue(AvaloniaProperty property, object? value)
|
|
{
|
|
if (TryGetEffectiveValue(property, out var existing))
|
|
{
|
|
existing.SetLocalValueAndRaise(this, property, value);
|
|
}
|
|
else
|
|
{
|
|
var effectiveValue = property.CreateEffectiveValue(Owner);
|
|
AddEffectiveValue(property, effectiveValue);
|
|
effectiveValue.SetLocalValueAndRaise(this, property, value);
|
|
}
|
|
}
|
|
|
|
public void SetLocalValue<T>(StyledProperty<T> property, T value)
|
|
{
|
|
if (TryGetEffectiveValue(property, out var existing))
|
|
{
|
|
var effective = (EffectiveValue<T>)existing;
|
|
effective.SetLocalValueAndRaise(this, property, value);
|
|
}
|
|
else
|
|
{
|
|
var effectiveValue = CreateEffectiveValue(property);
|
|
AddEffectiveValue(property, effectiveValue);
|
|
effectiveValue.SetLocalValueAndRaise(this, property, value);
|
|
}
|
|
}
|
|
|
|
public object? GetValue(AvaloniaProperty property)
|
|
{
|
|
if (_effectiveValues.TryGetValue(property, out var v))
|
|
return v.Value;
|
|
if (property.Inherits && TryGetInheritedValue(property, out v))
|
|
return v.Value;
|
|
|
|
return GetDefaultValue(property);
|
|
}
|
|
|
|
public T GetValue<T>(StyledProperty<T> property)
|
|
{
|
|
// <!> Performance critical method
|
|
if (_effectiveValues.TryGetValue(property, out var v))
|
|
return CastEffectiveValue<T>(v).Value;
|
|
if (property.Inherits && TryGetInheritedValue(property, out v))
|
|
return CastEffectiveValue<T>(v).Value;
|
|
return property.GetDefaultValue(Owner);
|
|
}
|
|
|
|
public BindingExpressionBase? GetExpression(AvaloniaProperty property)
|
|
{
|
|
var evaluatedLocalValue = false;
|
|
|
|
bool TryGetLocalValue(out BindingExpressionBase? result)
|
|
{
|
|
if (!evaluatedLocalValue)
|
|
{
|
|
evaluatedLocalValue = true;
|
|
|
|
if (_localValueBindings?.TryGetValue(property.Id, out var o) == true)
|
|
{
|
|
result = o as BindingExpressionBase;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
result = null;
|
|
return false;
|
|
}
|
|
|
|
for (var i = _frames.Count - 1; i >= 0; --i)
|
|
{
|
|
var frame = _frames[i];
|
|
|
|
if (frame.Priority > BindingPriority.LocalValue && TryGetLocalValue(out var localExpression))
|
|
return localExpression;
|
|
|
|
if (frame.TryGetEntryIfActive(property, out var entry, out _))
|
|
return entry as BindingExpressionBase;
|
|
}
|
|
|
|
TryGetLocalValue(out var e);
|
|
return e;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static EffectiveValue<T> CastEffectiveValue<T>(EffectiveValue value)
|
|
{
|
|
#if DEBUG
|
|
return (EffectiveValue<T>)value;
|
|
#else
|
|
// Avoid casts in release mode for performance since GetValue is a very hot path.
|
|
// We control every path:
|
|
// it shouldn't be possible to have something else than an EffectiveValue<T> stored for a T property.
|
|
return Unsafe.As<EffectiveValue<T>>(value);
|
|
#endif
|
|
}
|
|
|
|
public bool IsAnimating(AvaloniaProperty property)
|
|
{
|
|
if (_effectiveValues.TryGetValue(property, out var v))
|
|
return v.Priority <= BindingPriority.Animation;
|
|
return false;
|
|
}
|
|
|
|
public bool IsSet(AvaloniaProperty property) => _effectiveValues.TryGetValue(property, out _);
|
|
|
|
public void CoerceValue(AvaloniaProperty property)
|
|
{
|
|
if (_effectiveValues.TryGetValue(property, out var v))
|
|
v.CoerceValue(this, property);
|
|
else
|
|
property.RouteCoerceDefaultValue(Owner);
|
|
}
|
|
|
|
public void CoerceDefaultValue<T>(StyledProperty<T> property)
|
|
{
|
|
var metadata = property.GetMetadata(Owner);
|
|
|
|
if (metadata.CoerceValue is null)
|
|
return;
|
|
|
|
var coercedDefaultValue = metadata.CoerceValue(Owner, metadata.DefaultValue);
|
|
|
|
if (EqualityComparer<T>.Default.Equals(metadata.DefaultValue, coercedDefaultValue))
|
|
return;
|
|
|
|
// We have a situation where the default value isn't valid according to the coerce
|
|
// function. In this case, we need to create an EffectiveValue entry.
|
|
var effectiveValue = CreateEffectiveValue(property);
|
|
AddEffectiveValue(property, effectiveValue);
|
|
effectiveValue.SetCoercedDefaultValueAndRaise(this, property, coercedDefaultValue);
|
|
}
|
|
|
|
public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
|
|
{
|
|
if (TryGetEffectiveValue(property, out var v) &&
|
|
((EffectiveValue<T>)v).TryGetBaseValue(out var baseValue))
|
|
{
|
|
return baseValue;
|
|
}
|
|
|
|
return default;
|
|
}
|
|
|
|
public bool TryGetInheritedValue(
|
|
AvaloniaProperty property,
|
|
[NotNullWhen(true)] out EffectiveValue? result)
|
|
{
|
|
Debug.Assert(property.Inherits);
|
|
|
|
var i = InheritanceAncestor;
|
|
|
|
while (i is not null)
|
|
{
|
|
if (i.TryGetEffectiveValue(property, out result))
|
|
return true;
|
|
i = i.InheritanceAncestor;
|
|
}
|
|
|
|
result = null;
|
|
return false;
|
|
}
|
|
|
|
public EffectiveValue<T> CreateEffectiveValue<T>(StyledProperty<T> property)
|
|
{
|
|
EffectiveValue<T>? inherited = null;
|
|
|
|
if (property.Inherits && TryGetInheritedValue(property, out var v))
|
|
inherited = (EffectiveValue<T>)v;
|
|
|
|
return new EffectiveValue<T>(Owner, property, inherited);
|
|
}
|
|
|
|
public void SetInheritanceParent(AvaloniaObject? newParent)
|
|
{
|
|
var values = AvaloniaPropertyDictionaryPool<OldNewValue>.Get();
|
|
var oldAncestor = InheritanceAncestor;
|
|
var newAncestor = newParent?.GetValueStore();
|
|
|
|
if (newAncestor?._inheritedValueCount == 0)
|
|
newAncestor = newAncestor.InheritanceAncestor;
|
|
|
|
// The old and new inheritance ancestors are the same, nothing to do here.
|
|
if (oldAncestor == newAncestor)
|
|
return;
|
|
|
|
// First get the old values from the old inheritance ancestor.
|
|
var f = oldAncestor;
|
|
|
|
while (f is not null)
|
|
{
|
|
var count = f._effectiveValues.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
var value = f._effectiveValues.GetValue(i);
|
|
if (value.Property.Inherits)
|
|
values.TryAdd(value.Property, new(value));
|
|
}
|
|
|
|
f = f.InheritanceAncestor;
|
|
}
|
|
|
|
f = newAncestor;
|
|
|
|
// Get the new values from the new inheritance ancestor.
|
|
while (f is not null)
|
|
{
|
|
var count = f._effectiveValues.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
var value = f._effectiveValues.GetValue(i);
|
|
var property = value.Property;
|
|
|
|
if (!property.Inherits)
|
|
continue;
|
|
|
|
if (values.TryGetValue(property, out var existing))
|
|
{
|
|
if (existing.NewValue is null)
|
|
values[property] = existing.WithNewValue(value);
|
|
}
|
|
else
|
|
{
|
|
values.Add(property, new(null, value));
|
|
}
|
|
}
|
|
|
|
f = f.InheritanceAncestor;
|
|
}
|
|
|
|
OnInheritanceAncestorChanged(newAncestor);
|
|
|
|
// Raise PropertyChanged events where necessary on this object and inheritance children.
|
|
{
|
|
var count = values.Count;
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
var v = values.GetValue(i);
|
|
var oldValue = v.OldValue;
|
|
var newValue = v.NewValue;
|
|
|
|
if (oldValue != newValue)
|
|
{
|
|
var property = v.OldValue?.Property ?? v.NewValue!.Property;
|
|
InheritedValueChanged(property, oldValue, newValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
AvaloniaPropertyDictionaryPool<OldNewValue>.Release(values);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
|
|
/// binding produces a new value.
|
|
/// </summary>
|
|
/// <param name="entry">The binding entry.</param>
|
|
/// <param name="priority">The priority of binding which produced a new value.</param>
|
|
public void OnBindingValueChanged(
|
|
IValueEntry entry,
|
|
BindingPriority priority)
|
|
{
|
|
Debug.Assert(priority != BindingPriority.LocalValue);
|
|
|
|
var property = entry.Property;
|
|
|
|
if (TryGetEffectiveValue(property, out var existing))
|
|
{
|
|
if (priority <= existing.BasePriority)
|
|
ReevaluateEffectiveValue(property, existing, changedValueEntry: entry);
|
|
}
|
|
else
|
|
{
|
|
AddEffectiveValueAndRaise(property, entry, priority);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by a <see cref="ValueFrame"/> when its <see cref="ValueFrame.IsActive"/>
|
|
/// state changes.
|
|
/// </summary>
|
|
/// <param name="frame">The frame which produced the change.</param>
|
|
public void OnFrameActivationChanged(ValueFrame frame)
|
|
{
|
|
if (frame.EntryCount == 0)
|
|
return;
|
|
else if (frame.EntryCount == 1)
|
|
{
|
|
var property = frame.GetEntry(0).Property;
|
|
_effectiveValues.TryGetValue(property, out var current);
|
|
ReevaluateEffectiveValue(property, current);
|
|
}
|
|
else
|
|
{
|
|
ReevaluateEffectiveValues();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by the parent value store when its inheritance ancestor changes.
|
|
/// </summary>
|
|
/// <param name="ancestor">The new inheritance ancestor.</param>
|
|
public void OnInheritanceAncestorChanged(ValueStore? ancestor)
|
|
{
|
|
if (ancestor != this)
|
|
{
|
|
InheritanceAncestor = ancestor;
|
|
if (_inheritedValueCount > 0)
|
|
return;
|
|
}
|
|
|
|
var children = Owner.GetInheritanceChildren();
|
|
|
|
if (children is null)
|
|
return;
|
|
|
|
var count = children.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
children[i].GetValueStore().OnInheritanceAncestorChanged(ancestor);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
|
|
/// changes its value on this value store.
|
|
/// </summary>
|
|
/// <param name="property">The property whose value changed.</param>
|
|
/// <param name="oldValue">The old value of the property.</param>
|
|
/// <param name="value">The effective value instance.</param>
|
|
public void OnInheritedEffectiveValueChanged<T>(
|
|
StyledProperty<T> property,
|
|
T oldValue,
|
|
EffectiveValue<T> value)
|
|
{
|
|
Debug.Assert(property.Inherits);
|
|
|
|
var children = Owner.GetInheritanceChildren();
|
|
|
|
if (children is null)
|
|
return;
|
|
|
|
var count = children.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, value.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
|
|
/// is removed from the effective values.
|
|
/// </summary>
|
|
/// <param name="property">The property whose value changed.</param>
|
|
/// <param name="oldValue">The old value of the property.</param>
|
|
/// <param name="newValue">The new value of the property.</param>
|
|
public void OnInheritedEffectiveValueDisposed<T>(StyledProperty<T> property, T oldValue, T newValue)
|
|
{
|
|
Debug.Assert(property.Inherits);
|
|
|
|
var children = Owner.GetInheritanceChildren();
|
|
|
|
if (children is not null)
|
|
{
|
|
var count = children.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a <see cref="LocalValueBindingObserver{T}"/> or
|
|
/// <see cref="DirectBindingObserver{T}"/> completes.
|
|
/// </summary>
|
|
/// <param name="property">The previously bound property.</param>
|
|
/// <param name="observer">The observer.</param>
|
|
public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer)
|
|
{
|
|
if (_localValueBindings is not null &&
|
|
_localValueBindings.TryGetValue(property.Id, out var existing))
|
|
{
|
|
if (existing == observer)
|
|
{
|
|
_localValueBindings?.Remove(property.Id);
|
|
ClearValue(property);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when an inherited property changes on the value store of the inheritance ancestor.
|
|
/// </summary>
|
|
/// <typeparam name="T">The property type.</typeparam>
|
|
/// <param name="property">The property.</param>
|
|
/// <param name="oldValue">The old value of the property.</param>
|
|
/// <param name="newValue">The new value of the property.</param>
|
|
public void OnAncestorInheritedValueChanged<T>(
|
|
StyledProperty<T> property,
|
|
T oldValue,
|
|
T newValue)
|
|
{
|
|
Debug.Assert(property.Inherits);
|
|
|
|
// If the inherited value is set locally, propagation stops here.
|
|
if (_effectiveValues.ContainsKey(property))
|
|
return;
|
|
|
|
using var notifying = PropertyNotifying.Start(Owner, property);
|
|
|
|
Owner.RaisePropertyChanged(
|
|
property,
|
|
oldValue,
|
|
newValue,
|
|
BindingPriority.Inherited,
|
|
true);
|
|
|
|
var children = Owner.GetInheritanceChildren();
|
|
|
|
if (children is null)
|
|
return;
|
|
|
|
var count = children.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by a <see cref="ValueFrame"/> to re-evaluate the effective value when a value
|
|
/// is removed.
|
|
/// </summary>
|
|
/// <param name="frame">The frame on which the change occurred.</param>
|
|
/// <param name="property">The property whose value was removed.</param>
|
|
public void OnValueEntryRemoved(ValueFrame frame, AvaloniaProperty property)
|
|
{
|
|
if (frame.EntryCount == 0)
|
|
_frames.Remove(frame);
|
|
|
|
if (TryGetEffectiveValue(property, out var existing))
|
|
{
|
|
if (frame.Priority <= existing.Priority)
|
|
ReevaluateEffectiveValue(property, existing);
|
|
}
|
|
}
|
|
|
|
public bool RemoveFrame(ValueFrame frame)
|
|
{
|
|
if (_frames.Remove(frame))
|
|
{
|
|
frame.Dispose();
|
|
++_frameGeneration;
|
|
ReevaluateEffectiveValues();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void RemoveFrames(FrameType type)
|
|
{
|
|
var removed = false;
|
|
|
|
for (var i = _frames.Count - 1; i >= 0; --i)
|
|
{
|
|
var frame = _frames[i];
|
|
|
|
if (frame is not ImmediateValueFrame && frame.FramePriority.IsType(type))
|
|
{
|
|
_frames.RemoveAt(i);
|
|
frame.Dispose();
|
|
removed = true;
|
|
}
|
|
}
|
|
|
|
if (removed)
|
|
{
|
|
++_frameGeneration;
|
|
ReevaluateEffectiveValues();
|
|
}
|
|
}
|
|
|
|
|
|
public void RemoveFrames(IReadOnlyList<IStyle> styles)
|
|
{
|
|
var removed = false;
|
|
|
|
for (var i = _frames.Count - 1; i >= 0; --i)
|
|
{
|
|
var frame = _frames[i];
|
|
|
|
if (frame is StyleInstance style && styles.Contains(style.Source))
|
|
{
|
|
_frames.RemoveAt(i);
|
|
frame.Dispose();
|
|
removed = true;
|
|
}
|
|
}
|
|
|
|
if (removed)
|
|
{
|
|
++_frameGeneration;
|
|
ReevaluateEffectiveValues();
|
|
}
|
|
}
|
|
|
|
public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property)
|
|
{
|
|
object? value;
|
|
BindingPriority priority;
|
|
bool overridden = false;
|
|
|
|
if (_effectiveValues.TryGetValue(property, out var v))
|
|
{
|
|
value = v.Value;
|
|
priority = v.Priority;
|
|
overridden = v.IsOverridenCurrentValue;
|
|
}
|
|
else if (property.Inherits && TryGetInheritedValue(property, out v))
|
|
{
|
|
value = v.Value;
|
|
priority = BindingPriority.Inherited;
|
|
}
|
|
else
|
|
{
|
|
value = GetDefaultValue(property);
|
|
priority = BindingPriority.Unset;
|
|
}
|
|
|
|
return new AvaloniaPropertyValue(
|
|
property,
|
|
value,
|
|
priority,
|
|
null,
|
|
overridden);
|
|
}
|
|
|
|
void IBindingExpressionSink.OnChanged(
|
|
UntypedBindingExpressionBase instance,
|
|
bool hasValueChanged,
|
|
bool hasErrorChanged,
|
|
object? value,
|
|
BindingError? error)
|
|
{
|
|
Dispatcher.UIThread.VerifyAccess();
|
|
Debug.Assert(instance.TargetProperty is not null);
|
|
|
|
var property = instance.TargetProperty;
|
|
|
|
if (property.IsDirect)
|
|
{
|
|
if (hasValueChanged)
|
|
property.RouteSetDirectValueUnchecked(Owner, value);
|
|
}
|
|
else
|
|
{
|
|
var priority = instance.Priority;
|
|
|
|
if (hasValueChanged)
|
|
{
|
|
if (priority == BindingPriority.LocalValue)
|
|
{
|
|
if (value != AvaloniaProperty.UnsetValue)
|
|
SetLocalValue(property, value);
|
|
else if (property == StyledElement.DataContextProperty)
|
|
SetLocalValue(property, null);
|
|
else
|
|
ClearValue(property);
|
|
}
|
|
else
|
|
{
|
|
if (TryGetEffectiveValue(property, out var existing))
|
|
{
|
|
if (priority <= existing.BasePriority)
|
|
ReevaluateEffectiveValue(property, existing, changedValueEntry: instance);
|
|
}
|
|
else
|
|
{
|
|
AddEffectiveValueAndRaise(property, instance, priority);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasErrorChanged && instance.IsDataValidationEnabled)
|
|
{
|
|
var e = error?.ErrorType.ToBindingValueType() ?? BindingValueType.Value;
|
|
Owner.OnUpdateDataValidation(property, e, error?.Exception);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by a binding expression when the binding produces completes.
|
|
/// </summary>
|
|
/// <param name="instance">The binding expression.</param>
|
|
void IBindingExpressionSink.OnCompleted(UntypedBindingExpressionBase instance)
|
|
{
|
|
Dispatcher.UIThread.VerifyAccess();
|
|
Debug.Assert(instance.TargetProperty is not null);
|
|
|
|
var property = instance.TargetProperty;
|
|
|
|
if (instance.IsDataValidationEnabled)
|
|
Owner.OnUpdateDataValidation(property, BindingValueType.UnsetValue, null);
|
|
|
|
if (instance.Priority == BindingPriority.LocalValue)
|
|
{
|
|
if (_localValueBindings is not null &&
|
|
_localValueBindings.TryGetValue(property.Id, out var existing))
|
|
{
|
|
if (existing == instance)
|
|
{
|
|
_localValueBindings?.Remove(property.Id);
|
|
ClearValue(property);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public ValueStoreDiagnostic GetStoreDiagnostic()
|
|
{
|
|
var frames = new List<IValueFrameDiagnostic>();
|
|
|
|
var effectiveLocalValues = new List<ValueEntryDiagnostic>(_effectiveValues.Count);
|
|
for (var i = 0; i < _effectiveValues.Count; i++)
|
|
{
|
|
if (_effectiveValues.GetValue(i) is { } effectiveValue
|
|
&& effectiveValue.Priority == BindingPriority.LocalValue)
|
|
{
|
|
effectiveLocalValues.Add(new ValueEntryDiagnostic(effectiveValue.Property, effectiveValue.Value));
|
|
}
|
|
}
|
|
|
|
if (effectiveLocalValues.Count > 0)
|
|
{
|
|
frames.Add(new LocalValueFrameDiagnostic(effectiveLocalValues));
|
|
}
|
|
|
|
foreach (var frame in Frames)
|
|
{
|
|
if (frame is StyleInstance { Source: StyleBase } styleInstance)
|
|
{
|
|
frames.Add(new StyleValueFrameDiagnostic(styleInstance));
|
|
}
|
|
else
|
|
{
|
|
frames.Add(new ValueFrameDiagnostic(frame));
|
|
}
|
|
}
|
|
|
|
return new ValueStoreDiagnostic(frames);
|
|
}
|
|
|
|
private int InsertFrame(ValueFrame frame)
|
|
{
|
|
Debug.Assert(!_frames.Contains(frame));
|
|
|
|
var index = BinarySearchFrame(frame.FramePriority);
|
|
_frames.Insert(index, frame);
|
|
++_frameGeneration;
|
|
frame.SetOwner(this);
|
|
return index;
|
|
}
|
|
|
|
private ImmediateValueFrame GetOrCreateImmediateValueFrame(
|
|
AvaloniaProperty property,
|
|
BindingPriority priority,
|
|
out int frameIndex)
|
|
{
|
|
Debug.Assert(priority != BindingPriority.LocalValue);
|
|
|
|
var index = BinarySearchFrame(priority.ToFramePriority());
|
|
|
|
if (index > 0 && _frames[index - 1] is ImmediateValueFrame f &&
|
|
f.Priority == priority &&
|
|
!f.Contains(property))
|
|
{
|
|
frameIndex = index - 1;
|
|
return f;
|
|
}
|
|
|
|
var result = new ImmediateValueFrame(priority);
|
|
frameIndex = InsertFrame(result);
|
|
return result;
|
|
}
|
|
|
|
private void AddEffectiveValue(AvaloniaProperty property, EffectiveValue effectiveValue)
|
|
{
|
|
_effectiveValues.Add(property, effectiveValue);
|
|
|
|
if (property.Inherits && _inheritedValueCount++ == 0)
|
|
OnInheritanceAncestorChanged(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
|
|
/// event and notifies inheritance children if necessary .
|
|
/// </summary>
|
|
/// <param name="property">The property.</param>
|
|
/// <param name="entry">The value entry.</param>
|
|
/// <param name="priority">The value priority.</param>
|
|
private void AddEffectiveValueAndRaise(AvaloniaProperty property, IValueEntry entry, BindingPriority priority)
|
|
{
|
|
Debug.Assert(priority < BindingPriority.Inherited);
|
|
var effectiveValue = property.CreateEffectiveValue(Owner);
|
|
AddEffectiveValue(property, effectiveValue);
|
|
effectiveValue.SetAndRaise(this, entry, priority);
|
|
}
|
|
|
|
private void RemoveEffectiveValue(AvaloniaProperty property, int index)
|
|
{
|
|
_effectiveValues.RemoveAt(index);
|
|
if (property.Inherits && --_inheritedValueCount == 0)
|
|
OnInheritanceAncestorChanged(InheritanceAncestor);
|
|
}
|
|
|
|
private bool RemoveEffectiveValue(AvaloniaProperty property)
|
|
{
|
|
if (_effectiveValues.Remove(property))
|
|
{
|
|
if (property.Inherits && --_inheritedValueCount == 0)
|
|
OnInheritanceAncestorChanged(InheritanceAncestor);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void InheritedValueChanged(
|
|
AvaloniaProperty property,
|
|
EffectiveValue? oldValue,
|
|
EffectiveValue? newValue)
|
|
{
|
|
Debug.Assert(oldValue != newValue);
|
|
Debug.Assert(oldValue is not null || newValue is not null);
|
|
|
|
// If the value is set locally, propagation ends here.
|
|
if (_effectiveValues.ContainsKey(property) == true)
|
|
return;
|
|
|
|
using var notifying = PropertyNotifying.Start(Owner, property);
|
|
|
|
// Raise PropertyChanged on this object if necessary.
|
|
(oldValue ?? newValue!).RaiseInheritedValueChanged(Owner, property, oldValue, newValue);
|
|
|
|
var children = Owner.GetInheritanceChildren();
|
|
|
|
if (children is null)
|
|
return;
|
|
|
|
var count = children.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
children[i].GetValueStore().InheritedValueChanged(property, oldValue, newValue);
|
|
}
|
|
}
|
|
|
|
private void ReevaluateEffectiveValue(
|
|
AvaloniaProperty property,
|
|
EffectiveValue? current,
|
|
IValueEntry? changedValueEntry = null,
|
|
bool ignoreLocalValue = false)
|
|
{
|
|
++_isEvaluating;
|
|
|
|
try
|
|
{
|
|
restart:
|
|
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
|
|
// it has finished.
|
|
if (_styling > 0)
|
|
return;
|
|
|
|
var generation = _frameGeneration;
|
|
|
|
// Notify the existing effective value that reevaluation is starting.
|
|
current?.BeginReevaluation(ignoreLocalValue);
|
|
|
|
// Iterate the frames to get the effective value.
|
|
for (var i = _frames.Count - 1; i >= 0; --i)
|
|
{
|
|
var frame = _frames[i];
|
|
var priority = frame.Priority;
|
|
|
|
// Exit early if the current EffectiveValue has higher priority than this frame.
|
|
if (current?.Priority < priority && current?.BasePriority < priority)
|
|
break;
|
|
|
|
// Try to get an entry from the frame for the property we're reevaluating.
|
|
var foundEntry = frame.TryGetEntryIfActive(property, out var entry, out var activeChanged);
|
|
|
|
// If the active state of the frame has changed since the last read, and
|
|
// the frame holds multiple values then we need to re-evaluate the
|
|
// effective values of all properties.
|
|
if (activeChanged && frame.EntryCount > 1)
|
|
{
|
|
ReevaluateEffectiveValues(changedValueEntry);
|
|
return;
|
|
}
|
|
|
|
// If the frame has an entry for this property with a higher priority than the
|
|
// current effective value (and that entry has a value), then we have a new
|
|
// value for the property. Note that the check for entry.HasValue must be
|
|
// evaluated last as it can cause bindings to be subscribed.
|
|
if (foundEntry &&
|
|
HasHigherPriority(entry!, priority, current, changedValueEntry) &&
|
|
entry!.HasValue())
|
|
{
|
|
if (current is not null)
|
|
{
|
|
current.SetAndRaise(this, entry, priority);
|
|
}
|
|
else
|
|
{
|
|
current = property.CreateEffectiveValue(Owner);
|
|
AddEffectiveValue(property, current);
|
|
current.SetAndRaise(this, entry, priority);
|
|
}
|
|
}
|
|
|
|
if (generation != _frameGeneration)
|
|
goto restart;
|
|
}
|
|
|
|
if (current is not null)
|
|
{
|
|
current.EndReevaluation(this, property);
|
|
|
|
if (current.CanRemove())
|
|
{
|
|
if (current.BasePriority == BindingPriority.Unset)
|
|
{
|
|
RemoveEffectiveValue(property);
|
|
current.DisposeAndRaiseUnset(this, property);
|
|
}
|
|
else
|
|
{
|
|
current.RemoveAnimationAndRaise(this, property);
|
|
}
|
|
}
|
|
|
|
current.UnsubscribeIfNecessary();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
--_isEvaluating;
|
|
}
|
|
}
|
|
|
|
private void ReevaluateEffectiveValues(IValueEntry? changedValueEntry = null)
|
|
{
|
|
++_isEvaluating;
|
|
|
|
try
|
|
{
|
|
restart:
|
|
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
|
|
// it has finished.
|
|
if (_styling > 0)
|
|
return;
|
|
|
|
var generation = _frameGeneration;
|
|
var count = _effectiveValues.Count;
|
|
|
|
// Notify the existing effective values that reevaluation is starting.
|
|
for (var i = 0; i < count; ++i)
|
|
_effectiveValues[i].BeginReevaluation();
|
|
|
|
// Iterate the frames, setting and creating effective values.
|
|
for (var i = _frames.Count - 1; i >= 0; --i)
|
|
{
|
|
var frame = _frames[i];
|
|
|
|
if (!frame.IsActive())
|
|
continue;
|
|
|
|
var priority = frame.Priority;
|
|
|
|
count = frame.EntryCount;
|
|
|
|
for (var j = 0; j < count; ++j)
|
|
{
|
|
var entry = frame.GetEntry(j);
|
|
var property = entry.Property;
|
|
_effectiveValues.TryGetValue(property, out var effectiveValue);
|
|
|
|
if (!HasHigherPriority(entry, priority, effectiveValue, changedValueEntry))
|
|
continue;
|
|
|
|
if (!entry.HasValue())
|
|
continue;
|
|
|
|
if (effectiveValue is not null)
|
|
{
|
|
effectiveValue.SetAndRaise(this, entry, priority);
|
|
}
|
|
else
|
|
{
|
|
var v = property.CreateEffectiveValue(Owner);
|
|
AddEffectiveValue(property, v);
|
|
v.SetAndRaise(this, entry, priority);
|
|
}
|
|
|
|
if (generation != _frameGeneration)
|
|
goto restart;
|
|
}
|
|
}
|
|
|
|
// Remove all effective values that are still unset.
|
|
for (var i = _effectiveValues.Count - 1; i >= 0; --i)
|
|
{
|
|
var e = _effectiveValues.GetValue(i);
|
|
var property = e.Property;
|
|
|
|
e.EndReevaluation(this, property);
|
|
|
|
if (e.CanRemove())
|
|
{
|
|
RemoveEffectiveValue(property, i);
|
|
e.DisposeAndRaiseUnset(this, property);
|
|
|
|
if (i > _effectiveValues.Count)
|
|
break;
|
|
}
|
|
|
|
e.UnsubscribeIfNecessary();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
--_isEvaluating;
|
|
}
|
|
}
|
|
|
|
private static bool HasHigherPriority(
|
|
IValueEntry entry,
|
|
BindingPriority entryPriority,
|
|
EffectiveValue? current,
|
|
IValueEntry? changedValueEntry)
|
|
{
|
|
// Set the value if: there is no current effective value; or
|
|
if (current is null)
|
|
return true;
|
|
|
|
// The value's priority is higher than the current effective value's priority; or
|
|
if (entryPriority < current.Priority && entryPriority < current.BasePriority)
|
|
return true;
|
|
|
|
// - The value's priority is equal to the current effective value's priority
|
|
// - But the effective value was set via SetCurrentValue
|
|
// - As long as the SetCurrentValue wasn't overriding the value from the value entry under consideration
|
|
// - Or if it was, the value entry under consideration has changed; or
|
|
if (entryPriority == current.Priority &&
|
|
current.IsOverridenCurrentValue &&
|
|
(current.ValueEntry != entry || entry == changedValueEntry))
|
|
return true;
|
|
|
|
// The value is a non-animation value and its priority is higher than the current effective value's base
|
|
// priority.
|
|
if (entryPriority > BindingPriority.Animation && entryPriority < current.BasePriority)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryGetEffectiveValue(
|
|
AvaloniaProperty property,
|
|
[NotNullWhen(true)] out EffectiveValue? value)
|
|
{
|
|
return _effectiveValues.TryGetValue(property, out value);
|
|
}
|
|
|
|
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property)
|
|
{
|
|
if (_effectiveValues.TryGetValue(property, out var value))
|
|
return value;
|
|
return null;
|
|
}
|
|
|
|
private object? GetDefaultValue(AvaloniaProperty property)
|
|
{
|
|
return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner);
|
|
}
|
|
|
|
private void DisposeExistingLocalValueBinding(AvaloniaProperty property)
|
|
{
|
|
if (_localValueBindings is not null &&
|
|
_localValueBindings.TryGetValue(property.Id, out var existing))
|
|
{
|
|
existing.Dispose();
|
|
}
|
|
}
|
|
|
|
private int BinarySearchFrame(FramePriority priority)
|
|
{
|
|
var lo = 0;
|
|
var hi = _frames.Count - 1;
|
|
|
|
// Binary search insertion point.
|
|
while (lo <= hi)
|
|
{
|
|
var i = lo + ((hi - lo) >> 1);
|
|
var order = priority - _frames[i].FramePriority;
|
|
|
|
if (order <= 0)
|
|
{
|
|
lo = i + 1;
|
|
}
|
|
else
|
|
{
|
|
hi = i - 1;
|
|
}
|
|
}
|
|
|
|
return lo;
|
|
}
|
|
|
|
private readonly struct OldNewValue
|
|
{
|
|
public OldNewValue(EffectiveValue? oldValue)
|
|
{
|
|
OldValue = oldValue;
|
|
NewValue = null;
|
|
}
|
|
|
|
public OldNewValue(EffectiveValue? oldValue, EffectiveValue? newValue)
|
|
{
|
|
OldValue = oldValue;
|
|
NewValue = newValue;
|
|
}
|
|
|
|
public readonly EffectiveValue? OldValue;
|
|
public readonly EffectiveValue? NewValue;
|
|
|
|
public OldNewValue WithNewValue(EffectiveValue newValue) => new(OldValue, newValue);
|
|
}
|
|
}
|
|
}
|
|
|