using System; using Avalonia.Data; using Avalonia.Threading; #nullable enable namespace Avalonia.PropertyStore { /// /// Represents an untyped interface to . /// internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable { void Start(bool ignoreBatchUpdate); } /// /// Stores a binding in a or . /// /// The property type. internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserver> { private readonly IAvaloniaObject _owner; private IValueSink _sink; private IDisposable? _subscription; private bool _isSubscribed; private bool _batchUpdate; private Optional _value; public BindingEntry( IAvaloniaObject owner, StyledPropertyBase property, IObservable> source, BindingPriority priority, IValueSink sink) { _owner = owner; Property = property; Source = source; Priority = priority; _sink = sink; } public StyledPropertyBase Property { get; } public BindingPriority Priority { get; private set; } public IObservable> Source { get; } Optional IValue.GetValue() => _value.ToObject(); public void BeginBatchUpdate() => _batchUpdate = true; public void EndBatchUpdate() { _batchUpdate = false; if (_sink is ValueStore) Start(); } public Optional GetValue(BindingPriority maxPriority) { return Priority >= maxPriority ? _value : Optional.Empty; } public void Dispose() { _subscription?.Dispose(); _subscription = null; OnCompleted(); } public void OnCompleted() { var oldValue = _value; _value = default; Priority = BindingPriority.Unset; _isSubscribed = false; _sink.Completed(Property, this, oldValue); } public void OnError(Exception error) { throw new NotImplementedException(); } public void OnNext(BindingValue value) { if (Dispatcher.UIThread.CheckAccess()) { UpdateValue(value); } else { // To avoid allocating closure in the outer scope we need to capture variables // locally. This allows us to skip most of the allocations when on UI thread. var instance = this; var newValue = value; Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue)); } } public void Start() => Start(false); public void Start(bool ignoreBatchUpdate) { // We can't use _subscription to check whether we're subscribed because it won't be set // until Subscribe has finished, which will be too late to prevent reentrancy. In addition // don't re-subscribe to completed/disposed bindings (indicated by Unset priority). if (!_isSubscribed && Priority != BindingPriority.Unset && (!_batchUpdate || ignoreBatchUpdate)) { _isSubscribed = true; _subscription = Source.Subscribe(this); } } public void Reparent(IValueSink sink) => _sink = sink; public void RaiseValueChanged( IValueSink sink, IAvaloniaObject owner, AvaloniaProperty property, Optional oldValue, Optional newValue) { sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, oldValue.Cast(), newValue.Cast(), Priority)); } private void UpdateValue(BindingValue value) { if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) { value = Property.GetDefaultValue(_owner.GetType()); } if (value.Type == BindingValueType.DoNothing) { return; } var old = _value; if (value.Type != BindingValueType.DataValidationError) { _value = value.ToOptional(); } _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(_owner, Property, old, value, Priority)); } } }