Browse Source

Use SingleOrQueue instead of Queue in DeferredSetter. Make sure tests pass.

pull/1732/head
Jeremy Koritzinsky 8 years ago
parent
commit
00f8dfabc0
  1. 49
      src/Avalonia.Base/AvaloniaObject.cs
  2. 2
      src/Avalonia.Base/IPriorityValueOwner.cs
  3. 9
      src/Avalonia.Base/PriorityValue.cs
  4. 9
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  5. 54
      src/Avalonia.Base/Utilities/SingleOrQueue.cs
  6. 8
      src/Avalonia.Base/ValueStore.cs
  7. 52
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  8. 50
      tests/Avalonia.Base.UnitTests/Utilities/SingleOrQueueTests.cs

49
src/Avalonia.Base/AvaloniaObject.cs

@ -45,21 +45,8 @@ namespace Avalonia
/// </summary> /// </summary>
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged; private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
private ValueStore _values; private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
/// <summary>
/// Delayed setter helper for direct properties. Used to fix #855.
/// </summary>
private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
{
get
{
return _directDeferredSetter ??
(_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
}
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class. /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -223,9 +210,9 @@ namespace Avalonia
{ {
return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
} }
else if (_values != null) else if (Values != null)
{ {
var result = _values.GetValue(property); var result = Values.GetValue(property);
if (result == AvaloniaProperty.UnsetValue) if (result == AvaloniaProperty.UnsetValue)
{ {
@ -263,7 +250,7 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess(); VerifyAccess();
return _values?.IsAnimating(property) ?? false; return Values?.IsAnimating(property) ?? false;
} }
/// <summary> /// <summary>
@ -280,7 +267,7 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess(); VerifyAccess();
return _values?.IsSet(property) ?? false; return Values?.IsSet(property) ?? false;
} }
/// <summary> /// <summary>
@ -376,12 +363,7 @@ namespace Avalonia
description, description,
priority); priority);
if (_values == null) return Values.AddBinding(property, source, priority);
{
_values = new ValueStore(this);
}
return _values.AddBinding(property, source, priority);
} }
} }
@ -412,7 +394,7 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property) public void Revalidate(AvaloniaProperty property)
{ {
VerifyAccess(); VerifyAccess();
_values?.Revalidate(property); Values?.Revalidate(property);
} }
internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue) internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
@ -454,7 +436,7 @@ namespace Avalonia
/// Gets all priority values set on the object. /// Gets all priority values set on the object.
/// </summary> /// </summary>
/// <returns>A collection of property/value tuples.</returns> /// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => _values?.GetSetValues(); internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => Values?.GetSetValues();
/// <summary> /// <summary>
/// Forces revalidation of properties when a property value changes. /// Forces revalidation of properties when a property value changes.
@ -564,15 +546,15 @@ namespace Avalonia
T value) T value)
{ {
Contract.Requires<ArgumentNullException>(setterCallback != null); Contract.Requires<ArgumentNullException>(setterCallback != null);
return _values.Setter.SetAndNotify( return Values.Setter.SetAndNotify(
property, property,
ref field, ref field,
((object value, int) update, ref T backing, Action<Action> notify) => (object update, ref T backing, Action<Action> notify) =>
{ {
setterCallback((T)update.value, ref backing, notify); setterCallback((T)update, ref backing, notify);
return true; return true;
}, },
(value, 0)); value);
} }
/// <summary> /// <summary>
@ -735,13 +717,8 @@ namespace Avalonia
originalValue?.GetType().FullName ?? "(null)")); originalValue?.GetType().FullName ?? "(null)"));
} }
if (_values == null)
{
_values = new ValueStore(this);
}
LogPropertySet(property, value, priority); LogPropertySet(property, value, priority);
_values.AddValue(property, value, (int)priority); Values.AddValue(property, value, (int)priority);
} }
/// <summary> /// <summary>

2
src/Avalonia.Base/IPriorityValueOwner.cs

@ -33,6 +33,6 @@ namespace Avalonia
/// </summary> /// </summary>
void VerifyAccess(); void VerifyAccess();
DeferredSetter<AvaloniaProperty, (object value, int priority)> Setter { get; } DeferredSetter<AvaloniaProperty, object> Setter { get; }
} }
} }

9
src/Avalonia.Base/PriorityValue.cs

@ -248,6 +248,12 @@ namespace Avalonia
(value, priority)); (value, priority));
} }
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);
private bool UpdateCore( private bool UpdateCore(
(object value, int priority) update, (object value, int priority) update,
ref (object value, int priority) backing, ref (object value, int priority) backing,
@ -255,13 +261,14 @@ namespace Avalonia
{ {
var val = update.value; var val = update.value;
var notification = val as BindingNotification; var notification = val as BindingNotification;
object castValue;
if (notification != null) if (notification != null)
{ {
val = (notification.HasValue) ? notification.Value : null; val = (notification.HasValue) ? notification.Value : null;
} }
if (TypeUtilities.TryConvertImplicit(_valueType, val, out object castValue)) if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue))
{ {
var old = backing.value; var old = backing.value;

9
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -8,6 +8,7 @@ namespace Avalonia.Utilities
{ {
/// <summary> /// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent. /// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary> /// </summary>
/// <typeparam name="TProperty">The type of the object that represents the property.</typeparam> /// <typeparam name="TProperty">The type of the object that represents the property.</typeparam>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam> /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
@ -37,13 +38,13 @@ namespace Avalonia.Utilities
{ {
public bool Notifying { get; set; } public bool Notifying { get; set; }
private Queue<TSetRecord> pendingValues; private SingleOrQueue<TSetRecord> pendingValues;
public Queue<TSetRecord> PendingValues public SingleOrQueue<TSetRecord> PendingValues
{ {
get get
{ {
return pendingValues ?? (pendingValues = new Queue<TSetRecord>()); return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
} }
} }
} }
@ -89,7 +90,7 @@ namespace Avalonia.Utilities
/// <returns>If the property has any pending assignments.</returns> /// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(TProperty property) private bool HasPendingSet(TProperty property)
{ {
return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0; return setRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
} }
/// <summary> /// <summary>

54
src/Avalonia.Base/Utilities/SingleOrQueue.cs

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Utilities
{
public class SingleOrQueue<T>
{
private T _head;
private Queue<T> _tail;
private Queue<T> Tail => _tail ?? (_tail = new Queue<T>());
private bool HasTail => _tail != null;
public bool Empty { get; private set; } = true;
public void Enqueue(T value)
{
if (Empty)
{
_head = value;
}
else
{
Tail.Enqueue(value);
}
Empty = false;
}
public T Dequeue()
{
if (Empty)
{
throw new InvalidOperationException("Cannot dequeue from an empty queue!");
}
var result = _head;
if (HasTail && Tail.Count != 0)
{
_head = Tail.Dequeue();
}
else
{
_head = default;
Empty = true;
}
return result;
}
}
}

8
src/Avalonia.Base/ValueStore.cs

@ -116,7 +116,7 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property) public bool IsAnimating(AvaloniaProperty property)
{ {
return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false; return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
} }
public bool IsSet(AvaloniaProperty property) public bool IsSet(AvaloniaProperty property)
@ -168,14 +168,14 @@ namespace Avalonia
return value; return value;
} }
private DeferredSetter<AvaloniaProperty, (object value, int priority)> _defferedSetter; private DeferredSetter<AvaloniaProperty, object> _defferedSetter;
public DeferredSetter<AvaloniaProperty, (object value, int priority)> Setter public DeferredSetter<AvaloniaProperty, object> Setter
{ {
get get
{ {
return _defferedSetter ?? return _defferedSetter ??
(_defferedSetter = new DeferredSetter<AvaloniaProperty, (object value, int priority)>()); (_defferedSetter = new DeferredSetter<AvaloniaProperty, object>());
} }
} }
} }

52
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@ -1,11 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Utilities;
using Moq;
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests namespace Avalonia.Base.UnitTests
@ -21,7 +22,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Initial_Value_Should_Be_UnsetValue() public void Initial_Value_Should_Be_UnsetValue()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
Assert.Same(AvaloniaProperty.UnsetValue, target.Value); Assert.Same(AvaloniaProperty.UnsetValue, target.Value);
} }
@ -29,7 +30,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void First_Binding_Sets_Value() public void First_Binding_Sets_Value()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
@ -39,7 +40,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Changing_Binding_Should_Set_Value() public void Changing_Binding_Should_Set_Value()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<string>("foo"); var subject = new BehaviorSubject<string>("foo");
target.Add(subject, 0); target.Add(subject, 0);
@ -51,7 +52,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Setting_Direct_Value_Should_Override_Binding() public void Setting_Direct_Value_Should_Override_Binding()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
target.SetValue("bar", 0); target.SetValue("bar", 0);
@ -62,7 +63,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Binding_Firing_Should_Override_Direct_Value() public void Binding_Firing_Should_Override_Direct_Value()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("initial"); var source = new BehaviorSubject<object>("initial");
target.Add(source, 0); target.Add(source, 0);
@ -76,7 +77,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Earlier_Binding_Firing_Should_Not_Override_Later() public void Earlier_Binding_Firing_Should_Not_Override_Later()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var nonActive = new BehaviorSubject<object>("na"); var nonActive = new BehaviorSubject<object>("na");
var source = new BehaviorSubject<object>("initial"); var source = new BehaviorSubject<object>("initial");
@ -92,7 +93,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Binding_Completing_Should_Revert_To_Direct_Value() public void Binding_Completing_Should_Revert_To_Direct_Value()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("initial"); var source = new BehaviorSubject<object>("initial");
target.Add(source, 0); target.Add(source, 0);
@ -108,7 +109,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Binding_With_Lower_Priority_Has_Precedence() public void Binding_With_Lower_Priority_Has_Precedence()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 1); target.Add(Single("foo"), 1);
target.Add(Single("bar"), 0); target.Add(Single("bar"), 0);
@ -120,7 +121,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Later_Binding_With_Same_Priority_Should_Take_Precedence() public void Later_Binding_With_Same_Priority_Should_Take_Precedence()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 1); target.Add(Single("foo"), 1);
target.Add(Single("bar"), 0); target.Add(Single("bar"), 0);
@ -133,7 +134,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value() public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<string>("bar"); var subject = new BehaviorSubject<string>("bar");
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
@ -146,7 +147,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void UnsetValue_Should_Fall_Back_To_Next_Binding() public void UnsetValue_Should_Fall_Back_To_Next_Binding()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("bar"); var subject = new BehaviorSubject<object>("bar");
target.Add(subject, 0); target.Add(subject, 0);
@ -162,7 +163,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Adding_Value_Should_Call_OnNext() public void Adding_Value_Should_Call_OnNext()
{ {
var owner = new Mock<IPriorityValueOwner>(); var owner = GetMockOwner();
var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
@ -173,7 +174,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Changing_Value_Should_Call_OnNext() public void Changing_Value_Should_Call_OnNext()
{ {
var owner = new Mock<IPriorityValueOwner>(); var owner = GetMockOwner();
var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("foo"); var subject = new BehaviorSubject<object>("foo");
@ -186,7 +187,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Disposing_A_Binding_Should_Revert_To_Next_Value() public void Disposing_A_Binding_Should_Revert_To_Next_Value()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
var disposable = target.Add(Single("bar"), 0); var disposable = target.Add(Single("bar"), 0);
@ -199,7 +200,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Disposing_A_Binding_Should_Remove_BindingEntry() public void Disposing_A_Binding_Should_Remove_BindingEntry()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
var disposable = target.Add(Single("bar"), 0); var disposable = target.Add(Single("bar"), 0);
@ -212,7 +213,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Completing_A_Binding_Should_Revert_To_Previous_Binding() public void Completing_A_Binding_Should_Revert_To_Previous_Binding()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("bar"); var source = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
@ -226,7 +227,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Completing_A_Binding_Should_Revert_To_Lower_Priority() public void Completing_A_Binding_Should_Revert_To_Lower_Priority()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("bar"); var source = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 1); target.Add(Single("foo"), 1);
@ -240,7 +241,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Completing_A_Binding_Should_Remove_BindingEntry() public void Completing_A_Binding_Should_Remove_BindingEntry()
{ {
var target = new PriorityValue(null, TestProperty, typeof(string)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("bar"); var subject = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
@ -254,7 +255,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Direct_Value_Should_Be_Coerced() public void Direct_Value_Should_Be_Coerced()
{ {
var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, 10)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
target.SetValue(5, 0); target.SetValue(5, 0);
Assert.Equal(5, target.Value); Assert.Equal(5, target.Value);
@ -265,7 +266,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Bound_Value_Should_Be_Coerced() public void Bound_Value_Should_Be_Coerced()
{ {
var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, 10)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
var source = new Subject<object>(); var source = new Subject<object>();
target.Add(source, 0); target.Add(source, 0);
@ -279,7 +280,7 @@ namespace Avalonia.Base.UnitTests
public void Revalidate_Should_ReCoerce_Value() public void Revalidate_Should_ReCoerce_Value()
{ {
var max = 10; var max = 10;
var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, max)); var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, max));
var source = new Subject<object>(); var source = new Subject<object>();
target.Add(source, 0); target.Add(source, 0);
@ -302,5 +303,12 @@ namespace Avalonia.Base.UnitTests
{ {
return Observable.Never<T>().StartWith(value); return Observable.Never<T>().StartWith(value);
} }
private static Mock<IPriorityValueOwner> GetMockOwner()
{
var owner = new Mock<IPriorityValueOwner>();
owner.SetupGet(o => o.Setter).Returns(new DeferredSetter<AvaloniaProperty, object>());
return owner;
}
} }
} }

50
tests/Avalonia.Base.UnitTests/Utilities/SingleOrQueueTests.cs

@ -0,0 +1,50 @@
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
namespace Avalonia.Base.UnitTests.Utilities
{
public class SingleOrQueueTests
{
[Fact]
public void New_SingleOrQueue_Is_Empty()
{
Assert.True(new SingleOrQueue<object>().Empty);
}
[Fact]
public void Dequeue_Throws_When_Empty()
{
var queue = new SingleOrQueue<object>();
Assert.Throws<InvalidOperationException>(() => queue.Dequeue());
}
[Fact]
public void Enqueue_Adds_Element()
{
var queue = new SingleOrQueue<int>();
queue.Enqueue(1);
Assert.False(queue.Empty);
Assert.Equal(1, queue.Dequeue());
}
[Fact]
public void Multiple_Elements_Dequeued_In_Correct_Order()
{
var queue = new SingleOrQueue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
Assert.Equal(1, queue.Dequeue());
Assert.Equal(2, queue.Dequeue());
Assert.Equal(3, queue.Dequeue());
}
}
}
Loading…
Cancel
Save