From 9d4b73df0bb4f9f2a687f334c724ed1b1ccf0540 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Dec 2022 12:15:00 +0100 Subject: [PATCH] Marshal bindings to UI thread. --- .../PropertyStore/BindingEntryBase.cs | 45 +++++++++++----- .../LocalValueBindingObserver.cs | 53 ++++++++++++++++--- .../LocalValueUntypedBindingObserver.cs | 53 +++++++++++++------ 3 files changed, 113 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index ef14211902..b5ac5c817d 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -116,26 +117,42 @@ namespace Avalonia.PropertyStore private void SetValue(BindingValue value) { - if (Frame.Owner is null) - return; + static void Execute(BindingEntryBase instance, BindingValue value) + { + if (instance.Frame.Owner is null) + return; - LoggingUtils.LogIfNecessary(Frame.Owner.Owner, Property, value); + LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); - if (value.HasValue) - { - if (!_hasValue || !EqualityComparer.Default.Equals(_value, value.Value)) + if (value.HasValue) + { + if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value)) + { + instance._value = value.Value; + instance._hasValue = true; + if (instance._subscription is not null && instance._subscription != s_creatingQuiet) + instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); + } + } + else if (value.Type != BindingValueType.DoNothing) { - _value = value.Value; - _hasValue = true; - if (_subscription is not null && _subscription != s_creatingQuiet) - Frame.Owner?.OnBindingValueChanged(this, Frame.Priority); + instance.ClearValue(); + if (instance._subscription is not null && instance._subscription != s_creatingQuiet) + instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority); } } - else if (value.Type != BindingValueType.DoNothing) + + if (Dispatcher.UIThread.CheckAccess()) { - ClearValue(); - if (_subscription is not null && _subscription != s_creatingQuiet) - Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); + Execute(this, 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(() => Execute(instance, newValue)); } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 4dca6c0100..8acb885604 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -40,20 +41,56 @@ namespace Avalonia.PropertyStore public void OnNext(T value) { - if (Property.ValidateValue?.Invoke(value) != false) - _owner.SetValue(Property, value, BindingPriority.LocalValue); + static void Execute(ValueStore owner, StyledPropertyBase property, T value) + { + if (property.ValidateValue?.Invoke(value) != false) + owner.SetValue(property, value, BindingPriority.LocalValue); + else + owner.ClearLocalValue(property); + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(_owner, Property, value); + } else - _owner.ClearLocalValue(Property); + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = _owner; + var property = Property; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, property, newValue)); + } } public void OnNext(BindingValue value) { - LoggingUtils.LogIfNecessary(_owner.Owner, Property, value); + static void Execute(LocalValueBindingObserver instance, BindingValue value) + { + var owner = instance._owner; + var property = instance.Property; + + 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); + if (value.HasValue) + owner.SetValue(property, value.Value, BindingPriority.LocalValue); + else if (value.Type != BindingValueType.DataValidationError) + owner.ClearLocalValue(property); + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(this, 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(() => Execute(instance, newValue)); + } } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 099e997d38..7c529591b6 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -1,5 +1,7 @@ using System; +using System.Security.Cryptography; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -34,28 +36,47 @@ namespace Avalonia.PropertyStore public void OnNext(object? value) { - if (value is BindingNotification n) + static void Execute(LocalValueUntypedBindingObserver instance, object? value) { - value = n.Value; - LoggingUtils.LogIfNecessary(_owner.Owner, Property, n); - } + var owner = instance._owner; + var property = instance.Property; - if (value == AvaloniaProperty.UnsetValue) - { - _owner.ClearLocalValue(Property); - } - else if (value == BindingOperations.DoNothing) - { - // Do nothing! + if (value is BindingNotification n) + { + value = n.Value; + LoggingUtils.LogIfNecessary(owner.Owner, property, n); + } + + if (value == AvaloniaProperty.UnsetValue) + { + owner.ClearLocalValue(property); + } + else if (value == BindingOperations.DoNothing) + { + // Do nothing! + } + else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) + { + owner.SetValue(property, typedValue, BindingPriority.LocalValue); + } + else + { + owner.ClearLocalValue(property); + LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); + } } - else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) + + if (Dispatcher.UIThread.CheckAccess()) { - _owner.SetValue(Property, typedValue, BindingPriority.LocalValue); + Execute(this, value); } - else + else if (value != BindingOperations.DoNothing) { - _owner.ClearLocalValue(Property); - LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value); + // 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(() => Execute(instance, newValue)); } } }