Browse Source

Reworked bindings.

Now, two way bindings work as expected and setting a local value on a
property with a binding sets the value temporarily until the binding
changes value.
pull/58/head
Steven Kirk 11 years ago
parent
commit
a4c88ca764
  1. 17
      Perspex.Base/Diagnostics/IPerspexPropertyBinding.cs
  2. 17
      Perspex.Base/Diagnostics/PerspexPropertyBinding.cs
  3. 4
      Perspex.Base/Perspex.Base.csproj
  4. 42
      Perspex.Base/PerspexObject.cs
  5. 97
      Perspex.Base/PriorityBindingEntry.cs
  6. 121
      Perspex.Base/PriorityLevel.cs
  7. 316
      Perspex.Base/PriorityValue.cs
  8. 2
      Perspex.Themes.Default/TabControlStyle.cs
  9. 112
      Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs
  10. 120
      Tests/Perspex.Base.UnitTests/PriorityValueTests.cs

17
Perspex.Base/Diagnostics/IPerspexPropertyBinding.cs

@ -0,0 +1,17 @@
// -----------------------------------------------------------------------
// <copyright file="IPerspexPropertyBinding.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Diagnostics
{
public interface IPerspexPropertyBinding
{
string Description { get; }
int Priority { get; }
object Value { get; }
}
}

17
Perspex.Base/Diagnostics/PerspexPropertyBinding.cs

@ -0,0 +1,17 @@
// -----------------------------------------------------------------------
// <copyright file="PerspexPropertyBinding.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Diagnostics
{
internal class PerspexPropertyBinding : IPerspexPropertyBinding
{
public string Description { get; set; }
public int Priority { get; set; }
public object Value { get; set; }
}
}

4
Perspex.Base/Perspex.Base.csproj

@ -35,6 +35,9 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Binding.cs" />
<Compile Include="Diagnostics\PerspexPropertyBinding.cs" />
<Compile Include="Diagnostics\IPerspexPropertyBinding.cs" />
<Compile Include="PriorityBindingEntry.cs" />
<Compile Include="Collections\IPerspexList.cs" />
<Compile Include="Collections\PerspexReadOnlyListView.cs" />
<Compile Include="Collections\PerspexList.cs" />
@ -51,6 +54,7 @@
<Compile Include="Platform\IPlatformHandle.cs" />
<Compile Include="Platform\IPlatformThreadingInterface.cs" />
<Compile Include="Platform\PlatformHandle.cs" />
<Compile Include="PriorityLevel.cs" />
<Compile Include="PriorityValue.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Threading\Dispatcher.cs" />

42
Perspex.Base/PerspexObject.cs

@ -14,6 +14,8 @@ namespace Perspex
using System.Reflection;
using Perspex.Diagnostics;
using Splat;
using System.Reactive.Disposables;
/// <summary>
/// The priority of a binding.
@ -497,11 +499,14 @@ namespace Perspex
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
public void SetValue(PerspexProperty property, object value)
/// <param name="priority">The priority of the value.</param>
public void SetValue(
PerspexProperty property,
object value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
const int Priority = (int)BindingPriority.LocalValue;
PriorityValue v;
if (!this.IsRegistered(property))
@ -539,7 +544,7 @@ namespace Perspex
this.GetHashCode(),
value);
v.Replace(Observable.Never<object>().StartWith(value), Priority);
v.SetDirectValue(value, (int)priority);
}
/// <summary>
@ -548,11 +553,15 @@ namespace Perspex
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
public void SetValue<T>(PerspexProperty<T> property, T value)
/// <param name="priority">The priority of the value.</param>
public void SetValue<T>(
PerspexProperty<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
this.SetValue((PerspexProperty)property, value);
this.SetValue((PerspexProperty)property, value, priority);
}
/// <summary>
@ -588,14 +597,7 @@ namespace Perspex
this.GetHashCode(),
description != null ? description.Description : "[Anonymous]");
if (priority == BindingPriority.LocalValue)
{
return v.Replace(source, (int)priority);
}
else
{
return v.Add(source, (int)priority);
}
return v.Add(source, (int)priority);
}
/// <summary>
@ -624,20 +626,22 @@ namespace Perspex
/// <param name="property">The property on this object.</param>
/// <param name="source">The source object.</param>
/// <param name="sourceProperty">The property on the source object.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
/// <remarks>
/// The binding is first carried out from <paramref name="source"/> to this. Two-way
/// bindings are always at the LocalValue priority.
/// The binding is first carried out from <paramref name="source"/> to this.
/// </remarks>
public void BindTwoWay(
public IDisposable BindTwoWay(
PerspexProperty property,
PerspexObject source,
PerspexProperty sourceProperty)
PerspexProperty sourceProperty,
BindingPriority priority = BindingPriority.LocalValue)
{
source.GetObservable(sourceProperty).Subscribe(x => this.SetValue(property, x));
this.GetObservable(property).Subscribe(x => source.SetValue(sourceProperty, x));
return new CompositeDisposable(
this.Bind(property, source.GetObservable(sourceProperty)),
source.Bind(sourceProperty, this.GetObservable(property)));
}
/// <summary>

97
Perspex.Base/PriorityBindingEntry.cs

@ -0,0 +1,97 @@
// -----------------------------------------------------------------------
// <copyright file="PriorityValue.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex
{
using Perspex.Diagnostics;
using System;
/// <summary>
/// A registered binding in a <see cref="PriorityValue"/>.
/// </summary>
internal class PriorityBindingEntry : IDisposable
{
/// <summary>
/// The binding subscription.
/// </summary>
private IDisposable subscription;
public PriorityBindingEntry(int index)
{
this.Index = index;
}
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
private set;
}
public int Index
{
get;
}
/// <summary>
/// The current value of the binding.
/// </summary>
public object Value
{
get;
private set;
}
/// <summary>
/// Starts listening to the binding.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="changed">Called when the binding changes.</param>
/// <param name="completed">Called when the binding completes.</param>
public void Start(
IObservable<object> binding,
Action<PriorityBindingEntry> changed,
Action<PriorityBindingEntry> completed)
{
Contract.Requires<ArgumentNullException>(binding != null);
Contract.Requires<ArgumentNullException>(changed != null);
Contract.Requires<ArgumentNullException>(completed != null);
if (this.subscription != null)
{
throw new Exception("PriorityValue.Entry.Start() called more than once.");
}
this.Value = PerspexProperty.UnsetValue;
if (binding is IDescription)
{
this.Description = ((IDescription)binding).Description;
}
this.subscription = binding.Subscribe(
value =>
{
this.Value = value;
changed(this);
},
() => completed(this));
}
/// <summary>
/// Ends the binding subscription.
/// </summary>
public void Dispose()
{
if (this.subscription != null)
{
this.subscription.Dispose();
}
}
}
}

121
Perspex.Base/PriorityLevel.cs

@ -0,0 +1,121 @@
// -----------------------------------------------------------------------
// <copyright file="PriorityValueTests.cs" company="Steven Kirk">
// Copyright 2013 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex
{
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
internal class PriorityLevel
{
private Action<PriorityLevel> changed;
private object directValue;
private int nextIndex;
public PriorityLevel(
int priority,
Action<PriorityLevel> changed)
{
Contract.Requires<ArgumentNullException>(changed != null);
this.changed = changed;
this.Priority = priority;
this.Value = this.directValue = PerspexProperty.UnsetValue;
this.ActiveBindingIndex = -1;
this.Bindings = new LinkedList<PriorityBindingEntry>();
}
public int Priority { get; }
public object DirectValue
{
get
{
return this.directValue;
}
set
{
this.Value = this.directValue = value;
this.changed(this);
}
}
public object Value { get; private set; }
public int ActiveBindingIndex { get; private set; }
public LinkedList<PriorityBindingEntry> Bindings { get; }
public IDisposable Add(IObservable<object> binding)
{
Contract.Requires<ArgumentNullException>(binding != null);
var entry = new PriorityBindingEntry(this.nextIndex++);
var node = this.Bindings.AddFirst(entry);
entry.Start(binding, this.Changed, this.Completed);
return Disposable.Create(() =>
{
this.Bindings.Remove(node);
if (entry.Index >= this.ActiveBindingIndex)
{
this.ActivateFirstBinding();
}
});
}
private void Changed(PriorityBindingEntry entry)
{
if (entry.Index >= this.ActiveBindingIndex)
{
if (entry.Value != PerspexProperty.UnsetValue)
{
this.Value = entry.Value;
this.ActiveBindingIndex = entry.Index;
this.changed(this);
}
else
{
this.ActivateFirstBinding();
}
}
}
private void Completed(PriorityBindingEntry entry)
{
this.Bindings.Remove(entry);
if (entry.Index >= this.ActiveBindingIndex)
{
this.ActivateFirstBinding();
}
}
private void ActivateFirstBinding()
{
foreach (var binding in this.Bindings)
{
if (binding.Value != PerspexProperty.UnsetValue)
{
this.Value = binding.Value;
this.ActiveBindingIndex = binding.Index;
this.changed(this);
return;
}
}
this.Value = this.DirectValue;
this.ActiveBindingIndex = -1;
this.changed(this);
}
}
}

316
Perspex.Base/PriorityValue.cs

@ -6,9 +6,10 @@
namespace Perspex
{
using Perspex.Diagnostics;
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Linq;
using System.Reactive.Subjects;
using System.Reflection;
@ -21,8 +22,8 @@ namespace Perspex
/// represent higher priorites. The current <see cref="Value"/> is selected from the highest
/// priority binding that doesn't return <see cref="PerspexProperty.UnsetValue"/>. Where there
/// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes to a distinct new value, the
/// <see cref="Changed"/> observable is fired with the old and new values.
/// has a higher priority. Each time the value changes, the <see cref="Changed"/> observable is
/// fired with the old and new values.
/// </remarks>
public class PriorityValue
{
@ -37,9 +38,9 @@ namespace Perspex
private Type valueType;
/// <summary>
/// The currently registered binding entries.
/// The currently registered bindings organised by priority.
/// </summary>
private LinkedList<BindingEntry> bindings = new LinkedList<BindingEntry>();
private Dictionary<int, PriorityLevel> levels = new Dictionary<int, PriorityLevel>();
/// <summary>
/// The changed observable.
@ -51,6 +52,9 @@ namespace Perspex
/// </summary>
private object value;
/// <summary>
/// The function used to coerce the value, if any.
/// </summary>
private Func<object, object> coerce;
/// <summary>
@ -69,8 +73,11 @@ namespace Perspex
}
/// <summary>
/// Fired whenever the current <see cref="Value"/> changes to a new distinct value.
/// Fired whenever the current <see cref="Value"/> changes.
/// </summary>
/// <remarks>
/// The old and new values may be the same, this class does not check for distinct values.
/// </remarks>
public IObservable<Tuple<object, object>> Changed
{
get { return this.changed; }
@ -136,112 +143,39 @@ namespace Perspex
/// </returns>
public IDisposable Add(IObservable<object> binding, int priority)
{
BindingEntry entry = new BindingEntry();
LinkedListNode<BindingEntry> insert = this.bindings.First;
while (insert != null && insert.Value.Priority < priority)
{
insert = insert.Next;
}
if (insert == null)
{
this.bindings.AddLast(entry);
}
else
{
this.bindings.AddBefore(insert, entry);
}
entry.Start(binding, priority, this.EntryChanged, this.EntryCompleted);
return Disposable.Create(() =>
{
this.Remove(entry);
});
return this.GetLevel(priority).Add(binding);
}
/// <summary>
/// Adds a new binding, replacing all those of the same priority.
/// Sets the direct value for a specified priority.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="priority">The binding priority.</param>
/// <returns>
/// A disposable that will remove the binding.
/// </returns>
public IDisposable Replace(IObservable<object> binding, int priority)
/// <param name="value">The value.</param>
/// <param name="priority">The priority</param>
public void SetDirectValue(object value, int priority)
{
BindingEntry entry = new BindingEntry();
LinkedListNode<BindingEntry> insert = this.bindings.First;
while (insert != null && insert.Value.Priority < priority)
{
insert = insert.Next;
}
while (insert != null && insert.Value.Priority == priority)
{
LinkedListNode<BindingEntry> next = insert.Next;
insert.Value.Dispose();
this.bindings.Remove(insert);
insert = next;
}
if (insert == null)
{
this.bindings.AddLast(entry);
}
else
{
this.bindings.AddBefore(insert, entry);
}
entry.Start(binding, priority, this.EntryChanged, this.EntryCompleted);
return Disposable.Create(() =>
{
this.Remove(entry);
});
this.GetLevel(priority).DirectValue = value;
}
/// <summary>
/// Removes all bindings with the specified priority.
/// Gets the currently active bindings on this object.
/// </summary>
/// <param name="priority">The priority.</param>
public void Clear(int priority)
/// <returns>An enumerable collection of bindings.</returns>
public IEnumerable<IPerspexPropertyBinding> GetBindings()
{
LinkedListNode<BindingEntry> item = this.bindings.First;
bool removed = false;
while (item != null && item.Value.Priority <= priority)
foreach (var level in this.levels)
{
LinkedListNode<BindingEntry> next = item.Next;
if (item.Value.Priority == priority)
foreach (var binding in level.Value.Bindings)
{
item.Value.Dispose();
this.bindings.Remove(item);
removed = true;
yield return new PerspexPropertyBinding
{
Description = binding.Description,
Priority = level.Key,
Value = binding.Value,
};
}
item = next;
}
if (removed && priority <= this.ValuePriority)
{
this.UpdateValue();
}
}
/// <summary>
/// Gets the currently active bindings on this object.
/// </summary>
/// <returns>An enumerable collection of bindings.</returns>
public IEnumerable<BindingEntry> GetBindings()
{
return this.bindings;
}
/// <summary>
/// Causes a re-coercion of the value.
/// </summary>
@ -249,185 +183,95 @@ namespace Perspex
{
if (this.coerce != null)
{
this.SetValue(this.Value, this.ValuePriority);
}
}
PriorityLevel level;
/// <summary>
/// Throws an exception if <paramref name="value"/> is invalid.
/// </summary>
/// <param name="value">The value.</param>
private void VerifyValidValue(object value)
{
if (!IsValidValue(value, this.valueType))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
this.name,
value,
value.GetType().FullName));
if (this.levels.TryGetValue(this.ValuePriority, out level))
{
this.UpdateValue(level.Value, level.Priority);
}
}
}
/// <summary>
/// Called when a binding's value changes.
/// Gets the <see cref="PriorityLevel"/> with the specified priority, creating it if it
/// doesn't already exist.
/// </summary>
/// <param name="changed">The changed entry.</param>
private void EntryChanged(BindingEntry changed)
/// <param name="priority">The priority.</param>
/// <returns>The priority level.</returns>
private PriorityLevel GetLevel(int priority)
{
if (changed.Priority <= this.ValuePriority)
PriorityLevel result;
if (!this.levels.TryGetValue(priority, out result))
{
this.UpdateValue();
result = new PriorityLevel(priority, this.ValueChanged);
this.levels.Add(priority, result);
}
}
/// <summary>
/// Called when a binding completes.
/// </summary>
/// <param name="changed">The completed entry.</param>
private void EntryCompleted(BindingEntry entry)
{
this.Remove(entry);
return result;
}
/// <summary>
/// Sets the current value and notifies all observers.
/// Updates the current <see cref="Value"/> and notifies all subscibers.
/// </summary>
/// <param name="value">The new value.</param>
/// <param name="priority">The priority of the binding which produced the value.</param>
private void SetValue(object value, int priority)
/// <param name="value">The value to set.</param>
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
VerifyValidValue(value);
this.VerifyValidValue(value);
var old = this.value;
if (this.coerce != null)
{
value = this.coerce(value);
VerifyValidValue(value);
}
object old = this.value;
this.ValuePriority = priority;
if (!EqualityComparer<object>.Default.Equals(old, value))
{
this.value = value;
this.changed.OnNext(Tuple.Create(old, value));
}
this.value = value;
this.changed.OnNext(Tuple.Create(old, this.value));
}
/// <summary>
/// Removes the specified binding entry and updates the current value.
/// </summary>
/// <param name="entry">The binding entry to remove.</param>
private void Remove(BindingEntry entry)
{
entry.Dispose();
this.bindings.Remove(entry);
this.UpdateValue();
}
/// <summary>
/// Updates the current value.
/// Throws an exception if <paramref name="value"/> is invalid.
/// </summary>
private void UpdateValue()
/// <param name="value">The value.</param>
private void VerifyValidValue(object value)
{
foreach (BindingEntry entry in this.bindings)
if (!IsValidValue(value, this.valueType))
{
if (entry.Value != PerspexProperty.UnsetValue)
{
this.SetValue(entry.Value, entry.Priority);
return;
}
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
this.name,
value,
value.GetType().FullName));
}
this.SetValue(PerspexProperty.UnsetValue, int.MaxValue);
}
/// <summary>
/// A registered binding.
/// Called when the value for a priority level changes.
/// </summary>
public class BindingEntry : IDisposable
/// <param name="changed">The changed entry.</param>
private void ValueChanged(PriorityLevel level)
{
/// <summary>
/// The binding subscription.
/// </summary>
private IDisposable subscription;
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
if (level.Priority <= this.ValuePriority)
{
get;
private set;
}
/// <summary>
/// The priority of the binding.
/// </summary>
public int Priority
{
get;
private set;
}
/// <summary>
/// The current value of the binding.
/// </summary>
public object Value
{
get;
private set;
}
/// <summary>
/// Starts listening to the specified binding.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="priority">The binding priority.</param>
/// <param name="changed">Called when the binding changes.</param>
/// <param name="completed">Called when the binding completes.</param>
public void Start(
IObservable<object> binding,
int priority,
Action<BindingEntry> changed,
Action<BindingEntry> completed)
{
Contract.Requires<ArgumentNullException>(binding != null);
Contract.Requires<ArgumentNullException>(changed != null);
Contract.Requires<ArgumentNullException>(completed != null);
if (this.subscription != null)
if (level.Value != PerspexProperty.UnsetValue)
{
throw new Exception("PriorityValue.Entry.Start() called more than once.");
this.UpdateValue(level.Value, level.Priority);
}
this.Priority = priority;
this.Value = PerspexProperty.UnsetValue;
if (binding is IDescription)
else
{
this.Description = ((IDescription)binding).Description;
}
this.subscription = binding.Subscribe(
value =>
foreach (var i in this.levels.Values.OrderBy(x => x.Priority))
{
this.Value = value;
changed(this);
},
() => completed(this));
}
/// <summary>
/// Ends the binding subscription.
/// </summary>
public void Dispose()
{
if (this.subscription != null)
{
this.subscription.Dispose();
if (i.Value != PerspexProperty.UnsetValue)
{
this.UpdateValue(i.Value, i.Priority);
return;
}
}
this.UpdateValue(PerspexProperty.UnsetValue, int.MaxValue);
}
}
}

2
Perspex.Themes.Default/TabControlStyle.cs

@ -44,7 +44,7 @@ namespace Perspex.Themes.Default
{
Id = "tabStrip",
[~TabStrip.ItemsProperty] = control[~TabControl.ItemsProperty],
[~~TabStrip.SelectedTabProperty] = control[~~TabControl.SelectedTabProperty],
[~~TabStrip.SelectedItemProperty] = control[~~TabControl.SelectedItemProperty],
},
new ContentPresenter
{

112
Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs

@ -184,6 +184,19 @@ namespace Perspex.Base.UnitTests
Assert.Equal(10, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void SetValue_Respects_Priority()
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "one", BindingPriority.TemplatedParent);
Assert.Equal("one", target.GetValue(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "two", BindingPriority.Style);
Assert.Equal("one", target.GetValue(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "three", BindingPriority.StyleTrigger);
Assert.Equal("three", target.GetValue(Class1.FooProperty));
}
[Fact]
public void CoerceValue_Causes_Recoercion()
{
@ -348,42 +361,76 @@ namespace Perspex.Base.UnitTests
}
[Fact]
public void Binding_Doesnt_Set_Value_After_Clear()
public void Bind_Throws_Exception_For_Invalid_Value_Type()
{
Class1 target = new Class1();
Class1 source = new Class1();
source.SetValue(Class1.FooProperty, "initial");
target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty));
target.ClearValue(Class1.FooProperty);
source.SetValue(Class1.FooProperty, "newvalue");
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
Assert.Throws<InvalidOperationException>(() =>
{
target.Bind((PerspexProperty)Class1.FooProperty, Observable.Return((object)123));
});
}
[Fact]
public void Bind_Doesnt_Set_Value_After_Reset()
public void Two_Way_Binding_Works()
{
Class1 target = new Class1();
Class1 source = new Class1();
Class1 obj1 = new Class1();
Class1 obj2 = new Class1();
source.SetValue(Class1.FooProperty, "initial");
target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "reset");
source.SetValue(Class1.FooProperty, "newvalue");
obj1.SetValue(Class1.FooProperty, "initial1");
obj2.SetValue(Class1.FooProperty, "initial2");
obj1.Bind(Class1.FooProperty, obj2.GetObservable(Class1.FooProperty));
obj2.Bind(Class1.FooProperty, obj1.GetObservable(Class1.FooProperty));
Assert.Equal("initial2", obj1.GetValue(Class1.FooProperty));
Assert.Equal("initial2", obj2.GetValue(Class1.FooProperty));
obj1.SetValue(Class1.FooProperty, "first");
Assert.Equal("first", obj1.GetValue(Class1.FooProperty));
Assert.Equal("first", obj2.GetValue(Class1.FooProperty));
obj2.SetValue(Class1.FooProperty, "second");
Assert.Equal("second", obj1.GetValue(Class1.FooProperty));
Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
Assert.Equal("reset", target.GetValue(Class1.FooProperty));
obj1.SetValue(Class1.FooProperty, "third");
Assert.Equal("third", obj1.GetValue(Class1.FooProperty));
Assert.Equal("third", obj2.GetValue(Class1.FooProperty));
}
[Fact]
public void Bind_Throws_Exception_For_Invalid_Value_Type()
public void Two_Way_Binding_With_Priority_Works()
{
Class1 target = new Class1();
Class1 obj1 = new Class1();
Class1 obj2 = new Class1();
Assert.Throws<InvalidOperationException>(() =>
{
target.Bind((PerspexProperty)Class1.FooProperty, Observable.Return((object)123));
});
obj1.SetValue(Class1.FooProperty, "initial1", BindingPriority.Style);
obj2.SetValue(Class1.FooProperty, "initial2", BindingPriority.Style);
obj1.Bind(Class1.FooProperty, obj2.GetObservable(Class1.FooProperty), BindingPriority.Style);
obj2.Bind(Class1.FooProperty, obj1.GetObservable(Class1.FooProperty), BindingPriority.Style);
Assert.Equal("initial2", obj1.GetValue(Class1.FooProperty));
Assert.Equal("initial2", obj2.GetValue(Class1.FooProperty));
obj1.SetValue(Class1.FooProperty, "first", BindingPriority.Style);
Assert.Equal("first", obj1.GetValue(Class1.FooProperty));
Assert.Equal("first", obj2.GetValue(Class1.FooProperty));
obj2.SetValue(Class1.FooProperty, "second", BindingPriority.Style);
Assert.Equal("second", obj1.GetValue(Class1.FooProperty));
Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
obj1.SetValue(Class1.FooProperty, "third", BindingPriority.Style);
Assert.Equal("third", obj1.GetValue(Class1.FooProperty));
Assert.Equal("third", obj2.GetValue(Class1.FooProperty));
}
[Fact]
@ -426,24 +473,31 @@ namespace Perspex.Base.UnitTests
}
[Fact]
public void StyleBinding_Overrides_Default_Value()
public void Local_Binding_Overwrites_Local_Value()
{
Class1 target = new Class1();
var target = new Class1();
var binding = new Subject<string>();
target.Bind(Class1.FooProperty, this.Single("stylevalue"), BindingPriority.Style);
target.Bind(Class1.FooProperty, binding);
Assert.Equal("stylevalue", target.GetValue(Class1.FooProperty));
binding.OnNext("first");
Assert.Equal("first", target.GetValue(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "second");
Assert.Equal("second", target.GetValue(Class1.FooProperty));
binding.OnNext("third");
Assert.Equal("third", target.GetValue(Class1.FooProperty));
}
[Fact]
public void StyleBinding_Doesnt_Override_Local_Value()
public void StyleBinding_Overrides_Default_Value()
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "newvalue");
target.Bind(Class1.FooProperty, this.Single("stylevalue"), BindingPriority.Style);
Assert.Equal("newvalue", target.GetValue(Class1.FooProperty));
Assert.Equal("stylevalue", target.GetValue(Class1.FooProperty));
}
[Fact]

120
Tests/Perspex.Base.UnitTests/PriorityValueTests.cs

@ -44,6 +44,63 @@ namespace Perspex.Base.UnitTests
Assert.Equal("bar", target.Value);
}
[Fact]
public void Setting_Direct_Value_Should_Override_Binding()
{
var target = new PriorityValue("Test", typeof(string));
target.Add(this.Single("foo"), 0);
target.SetDirectValue("bar", 0);
Assert.Equal("bar", target.Value);
}
[Fact]
public void Binding_Firing_Should_Override_Direct_Value()
{
var target = new PriorityValue("Test", typeof(string));
var source = new BehaviorSubject<object>("initial");
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
Assert.Equal("first", target.Value);
source.OnNext("second");
Assert.Equal("second", target.Value);
}
[Fact]
public void Non_Active_Binding_Firing_Should_Not_Override_Direct_Value()
{
var target = new PriorityValue("Test", typeof(string));
var nonActive = new BehaviorSubject<object>("na");
var source = new BehaviorSubject<object>("initial");
target.Add(nonActive, 0);
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
Assert.Equal("first", target.Value);
nonActive.OnNext("second");
Assert.Equal("first", target.Value);
}
[Fact]
public void Binding_Completing_Should_Revert_To_Direct_Value()
{
var target = new PriorityValue("Test", typeof(string));
var source = new BehaviorSubject<object>("initial");
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
Assert.Equal("first", target.Value);
source.OnNext("second");
Assert.Equal("second", target.Value);
source.OnCompleted();
Assert.Equal("first", target.Value);
}
[Fact]
public void Binding_With_Lower_Priority_Has_Precedence()
{
@ -151,16 +208,30 @@ namespace Perspex.Base.UnitTests
}
[Fact]
public void Completing_A_Binding_Should_Revert_To_Next_Value()
public void Completing_A_Binding_Should_Revert_To_Previous_Binding()
{
var target = new PriorityValue("Test", typeof(string));
var subject = new BehaviorSubject<object>("bar");
var source = new BehaviorSubject<object>("bar");
target.Add(this.Single("foo"), 0);
target.Add(subject, 0);
target.Add(source, 0);
Assert.Equal("bar", target.Value);
subject.OnCompleted();
source.OnCompleted();
Assert.Equal("foo", target.Value);
}
[Fact]
public void Completing_A_Binding_Should_Revert_To_Lower_Priority()
{
var target = new PriorityValue("Test", typeof(string));
var source = new BehaviorSubject<object>("bar");
target.Add(this.Single("foo"), 1);
target.Add(source, 0);
Assert.Equal("bar", target.Value);
source.OnCompleted();
Assert.Equal("foo", target.Value);
}
@ -178,6 +249,47 @@ namespace Perspex.Base.UnitTests
Assert.Equal(1, target.GetBindings().Count());
}
[Fact]
public void Direct_Value_Should_Be_Coerced()
{
var target = new PriorityValue("Test", typeof(int), x => Math.Min((int)x, 10));
target.SetDirectValue(5, 0);
Assert.Equal(5, target.Value);
target.SetDirectValue(15, 0);
Assert.Equal(10, target.Value);
}
[Fact]
public void Bound_Value_Should_Be_Coerced()
{
var target = new PriorityValue("Test", typeof(int), x => Math.Min((int)x, 10));
var source = new Subject<object>();
target.Add(source, 0);
source.OnNext(5);
Assert.Equal(5, target.Value);
source.OnNext(15);
Assert.Equal(10, target.Value);
}
[Fact]
public void Coerce_Should_ReCoerce_Value()
{
var max = 10;
var target = new PriorityValue("Test", typeof(int), x => Math.Min((int)x, max));
var source = new Subject<object>();
target.Add(source, 0);
source.OnNext(5);
Assert.Equal(5, target.Value);
source.OnNext(15);
Assert.Equal(10, target.Value);
max = 12;
target.Coerce();
Assert.Equal(12, target.Value);
}
/// <summary>
/// Returns an observable that returns a single value but does not complete.
/// </summary>

Loading…
Cancel
Save