diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 2583b7ac97..12b7e3d529 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -594,8 +594,19 @@ namespace Avalonia } } - protected void SetAndRaise(AvaloniaProperty property, Action> setterCallback, T value, Predicate pendingSetCondition = null) - => directDelayedSetter.SetAndNotify(property, (val, notify) => setterCallback((T)val, notify), value, o => pendingSetCondition?.Invoke((T)o) ?? true); + protected void SetAndRaise( + AvaloniaProperty property, + Action> setterCallback, + T value, + Predicate pendingSetCondition = null) + { + Contract.Requires(setterCallback != null); + directDelayedSetter.SetAndNotify( + property, + (val, notify) => setterCallback((T)val, notify), + value, + pendingSetCondition != null ? o => pendingSetCondition((T)o) : (Predicate)null); + } /// /// Tries to cast a value to a type, taking into account that the value may be a diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 2a6c5bc2c6..5d223c3cf8 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -235,56 +235,61 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - delayedSetter.SetAndNotify(this, (update, notify) => + delayedSetter.SetAndNotify(this, + UpdateCore, + (value, priority), + val => !object.Equals(val.value, _value)); + } + + private void UpdateCore((object value, int priority) update, Action notify) + { + var val = update.value; + var notification = val as BindingNotification; + object castValue; + + if (notification != null) { - var val = update.value; - var notification = val as BindingNotification; - object castValue; + val = (notification.HasValue) ? notification.Value : null; + } - if (notification != null) - { - val = (notification.HasValue) ? notification.Value : null; - } + if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue)) + { + var old = _value; - if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue)) + if (_validate != null && castValue != AvaloniaProperty.UnsetValue) { - var old = _value; - - if (_validate != null && castValue != AvaloniaProperty.UnsetValue) - { - castValue = _validate(castValue); - } + castValue = _validate(castValue); + } - ValuePriority = priority; - _value = castValue; + ValuePriority = update.priority; + _value = castValue; - if (notification?.HasValue == true) - { - notification.SetValue(castValue); - } - - if (notification == null || notification.HasValue) - { - notify(() => Owner?.Changed(this, old, _value)); - } + if (notification?.HasValue == true) + { + notification.SetValue(castValue); + } - if (notification != null) - { - Owner?.BindingNotificationReceived(this, notification); - } + if (notification == null || notification.HasValue) + { + notify(() => Owner?.Changed(this, old, _value)); } - else + + if (notification != null) { - Logger.Error( - LogArea.Binding, - Owner, - "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", - Property.Name, - _valueType, - val, - val?.GetType()); + Owner?.BindingNotificationReceived(this, notification); } - }, (value, priority), val => !object.Equals(val.value, _value)); + } + else + { + Logger.Error( + LogArea.Binding, + Owner, + "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", + Property.Name, + _valueType, + val, + val?.GetType()); + } } } } diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 4b2709c141..b87711c1d9 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using System.Runtime.CompilerServices; using System.Text; namespace Avalonia.Utilities @@ -11,7 +12,24 @@ namespace Avalonia.Utilities /// The type of the object that represents the property. /// The type of value with which to track the delayed assignment. class DeferredSetter + where TProperty: class { + private struct NotifyDisposable : IDisposable + { + private readonly SettingStatus status; + + public NotifyDisposable(SettingStatus status) + { + this.status = status; + status.Notifying = true; + } + + public void Dispose() + { + status.Notifying = false; + } + } + /// /// Information on current setting/notification status of a property. /// @@ -30,7 +48,7 @@ namespace Avalonia.Utilities } } - private readonly Dictionary setRecords = new Dictionary(); + private readonly ConditionalWeakTable setRecords = new ConditionalWeakTable(); /// /// Mark the property as currently notifying. @@ -40,14 +58,8 @@ namespace Avalonia.Utilities internal IDisposable MarkNotifying(TProperty property) { Contract.Requires(!IsNotifying(property)); - - if (!setRecords.ContainsKey(property)) - { - setRecords[property] = new SettingStatus(); - } - setRecords[property].Notifying = true; - - return Disposable.Create(() => setRecords[property].Notifying = false); + + return new NotifyDisposable(setRecords.GetOrCreateValue(property)); } /// @@ -66,11 +78,8 @@ namespace Avalonia.Utilities internal void AddPendingSet(TProperty property, TValue value) { Contract.Requires(IsNotifying(property)); - if (!setRecords.ContainsKey(property)) - { - setRecords[property] = new SettingStatus(); - } - setRecords[property].PendingValues.Enqueue(value); + + setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value); } /// @@ -80,7 +89,7 @@ namespace Avalonia.Utilities /// If the property has any pending assignments. internal bool HasPendingSet(TProperty property) { - return setRecords.ContainsKey(property) && setRecords[property].PendingValues.Count != 0; + return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0; } /// @@ -90,7 +99,7 @@ namespace Avalonia.Utilities /// The first pending assignment for the property. internal TValue GetFirstPendingSet(TProperty property) { - return setRecords[property].PendingValues.Dequeue(); + return setRecords.GetOrCreateValue(property).PendingValues.Dequeue(); } /// @@ -103,7 +112,11 @@ namespace Avalonia.Utilities /// /// The value to try to set. /// A predicate to filter what possible values should be added as pending sets (i.e. only values not equal to the current value). - public void SetAndNotify(TProperty property, Action> setterCallback, TValue value, Predicate pendingSetCondition) + public void SetAndNotify( + TProperty property, + Action> setterCallback, + TValue value, + Predicate pendingSetCondition) { Contract.Requires(setterCallback != null); if (!IsNotifying(property)) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 529e35d92c..cb61a663a1 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -406,7 +406,7 @@ namespace Avalonia.Base.UnitTests Assert.Equal(1, viewModel.SetterInvokedCount); - //here in real life stack overflow exception is thrown issue #855 and #824 + // Issues #855 and #824 were causing a StackOverflowException at this point. target.DoubleValue = 51.001; Assert.Equal(2, viewModel.SetterInvokedCount);