A cross-platform UI framework for .NET
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.
 
 
 

507 lines
18 KiB

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;
}
}
}
}