From a4c88ca764cb50aae4ebfb275a7c6903244298b4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 May 2015 18:50:19 +0200 Subject: [PATCH] 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. --- .../Diagnostics/IPerspexPropertyBinding.cs | 17 + .../Diagnostics/PerspexPropertyBinding.cs | 17 + Perspex.Base/Perspex.Base.csproj | 4 + Perspex.Base/PerspexObject.cs | 42 +-- Perspex.Base/PriorityBindingEntry.cs | 97 ++++++ Perspex.Base/PriorityLevel.cs | 121 +++++++ Perspex.Base/PriorityValue.cs | 316 +++++------------- Perspex.Themes.Default/TabControlStyle.cs | 2 +- .../PerspexObjectTests.cs | 112 +++++-- .../PriorityValueTests.cs | 120 ++++++- 10 files changed, 559 insertions(+), 289 deletions(-) create mode 100644 Perspex.Base/Diagnostics/IPerspexPropertyBinding.cs create mode 100644 Perspex.Base/Diagnostics/PerspexPropertyBinding.cs create mode 100644 Perspex.Base/PriorityBindingEntry.cs create mode 100644 Perspex.Base/PriorityLevel.cs diff --git a/Perspex.Base/Diagnostics/IPerspexPropertyBinding.cs b/Perspex.Base/Diagnostics/IPerspexPropertyBinding.cs new file mode 100644 index 0000000000..a27cc7e902 --- /dev/null +++ b/Perspex.Base/Diagnostics/IPerspexPropertyBinding.cs @@ -0,0 +1,17 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Diagnostics +{ + public interface IPerspexPropertyBinding + { + string Description { get; } + + int Priority { get; } + + object Value { get; } + } +} \ No newline at end of file diff --git a/Perspex.Base/Diagnostics/PerspexPropertyBinding.cs b/Perspex.Base/Diagnostics/PerspexPropertyBinding.cs new file mode 100644 index 0000000000..771fb73b40 --- /dev/null +++ b/Perspex.Base/Diagnostics/PerspexPropertyBinding.cs @@ -0,0 +1,17 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Diagnostics +{ + internal class PerspexPropertyBinding : IPerspexPropertyBinding + { + public string Description { get; set; } + + public int Priority { get; set; } + + public object Value { get; set; } + } +} \ No newline at end of file diff --git a/Perspex.Base/Perspex.Base.csproj b/Perspex.Base/Perspex.Base.csproj index 0ef4c82d26..fdba33905a 100644 --- a/Perspex.Base/Perspex.Base.csproj +++ b/Perspex.Base/Perspex.Base.csproj @@ -35,6 +35,9 @@ + + + @@ -51,6 +54,7 @@ + diff --git a/Perspex.Base/PerspexObject.cs b/Perspex.Base/PerspexObject.cs index 0783210239..564698a54b 100644 --- a/Perspex.Base/PerspexObject.cs +++ b/Perspex.Base/PerspexObject.cs @@ -14,6 +14,8 @@ namespace Perspex using System.Reflection; using Perspex.Diagnostics; using Splat; + using System.Reactive.Disposables; + /// /// The priority of a binding. @@ -497,11 +499,14 @@ namespace Perspex /// /// The property. /// The value. - public void SetValue(PerspexProperty property, object value) + /// The priority of the value. + public void SetValue( + PerspexProperty property, + object value, + BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(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().StartWith(value), Priority); + v.SetDirectValue(value, (int)priority); } /// @@ -548,11 +553,15 @@ namespace Perspex /// The type of the property. /// The property. /// The value. - public void SetValue(PerspexProperty property, T value) + /// The priority of the value. + public void SetValue( + PerspexProperty property, + T value, + BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); - this.SetValue((PerspexProperty)property, value); + this.SetValue((PerspexProperty)property, value, priority); } /// @@ -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); } /// @@ -624,20 +626,22 @@ namespace Perspex /// The property on this object. /// The source object. /// The property on the source object. + /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// /// - /// The binding is first carried out from to this. Two-way - /// bindings are always at the LocalValue priority. + /// The binding is first carried out from to this. /// - 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))); } /// diff --git a/Perspex.Base/PriorityBindingEntry.cs b/Perspex.Base/PriorityBindingEntry.cs new file mode 100644 index 0000000000..5e2ff6a018 --- /dev/null +++ b/Perspex.Base/PriorityBindingEntry.cs @@ -0,0 +1,97 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex +{ + using Perspex.Diagnostics; + using System; + + /// + /// A registered binding in a . + /// + internal class PriorityBindingEntry : IDisposable + { + /// + /// The binding subscription. + /// + private IDisposable subscription; + + public PriorityBindingEntry(int index) + { + this.Index = index; + } + + /// + /// Gets a description of the binding. + /// + public string Description + { + get; + private set; + } + + public int Index + { + get; + } + + /// + /// The current value of the binding. + /// + public object Value + { + get; + private set; + } + + /// + /// Starts listening to the binding. + /// + /// The binding. + /// Called when the binding changes. + /// Called when the binding completes. + public void Start( + IObservable binding, + Action changed, + Action completed) + { + Contract.Requires(binding != null); + Contract.Requires(changed != null); + Contract.Requires(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)); + } + + /// + /// Ends the binding subscription. + /// + public void Dispose() + { + if (this.subscription != null) + { + this.subscription.Dispose(); + } + } + } +} diff --git a/Perspex.Base/PriorityLevel.cs b/Perspex.Base/PriorityLevel.cs new file mode 100644 index 0000000000..e973af4353 --- /dev/null +++ b/Perspex.Base/PriorityLevel.cs @@ -0,0 +1,121 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex +{ + using System; + using System.Collections.Generic; + using System.Reactive.Disposables; + + internal class PriorityLevel + { + private Action changed; + + private object directValue; + + private int nextIndex; + + public PriorityLevel( + int priority, + Action changed) + { + Contract.Requires(changed != null); + + this.changed = changed; + this.Priority = priority; + this.Value = this.directValue = PerspexProperty.UnsetValue; + this.ActiveBindingIndex = -1; + this.Bindings = new LinkedList(); + } + + 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 Bindings { get; } + + public IDisposable Add(IObservable binding) + { + Contract.Requires(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); + } + } +} diff --git a/Perspex.Base/PriorityValue.cs b/Perspex.Base/PriorityValue.cs index ccfcb628de..d1c429a810 100644 --- a/Perspex.Base/PriorityValue.cs +++ b/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 is selected from the highest /// priority binding that doesn't return . 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 - /// observable is fired with the old and new values. + /// has a higher priority. Each time the value changes, the observable is + /// fired with the old and new values. /// public class PriorityValue { @@ -37,9 +38,9 @@ namespace Perspex private Type valueType; /// - /// The currently registered binding entries. + /// The currently registered bindings organised by priority. /// - private LinkedList bindings = new LinkedList(); + private Dictionary levels = new Dictionary(); /// /// The changed observable. @@ -51,6 +52,9 @@ namespace Perspex /// private object value; + /// + /// The function used to coerce the value, if any. + /// private Func coerce; /// @@ -69,8 +73,11 @@ namespace Perspex } /// - /// Fired whenever the current changes to a new distinct value. + /// Fired whenever the current changes. /// + /// + /// The old and new values may be the same, this class does not check for distinct values. + /// public IObservable> Changed { get { return this.changed; } @@ -136,112 +143,39 @@ namespace Perspex /// public IDisposable Add(IObservable binding, int priority) { - BindingEntry entry = new BindingEntry(); - LinkedListNode 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); } /// - /// Adds a new binding, replacing all those of the same priority. + /// Sets the direct value for a specified priority. /// - /// The binding. - /// The binding priority. - /// - /// A disposable that will remove the binding. - /// - public IDisposable Replace(IObservable binding, int priority) + /// The value. + /// The priority + public void SetDirectValue(object value, int priority) { - BindingEntry entry = new BindingEntry(); - LinkedListNode insert = this.bindings.First; - - while (insert != null && insert.Value.Priority < priority) - { - insert = insert.Next; - } - - while (insert != null && insert.Value.Priority == priority) - { - LinkedListNode 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; } /// - /// Removes all bindings with the specified priority. + /// Gets the currently active bindings on this object. /// - /// The priority. - public void Clear(int priority) + /// An enumerable collection of bindings. + public IEnumerable GetBindings() { - LinkedListNode item = this.bindings.First; - bool removed = false; - - while (item != null && item.Value.Priority <= priority) + foreach (var level in this.levels) { - LinkedListNode 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(); } } - /// - /// Gets the currently active bindings on this object. - /// - /// An enumerable collection of bindings. - public IEnumerable GetBindings() - { - return this.bindings; - } - /// /// Causes a re-coercion of the value. /// @@ -249,185 +183,95 @@ namespace Perspex { if (this.coerce != null) { - this.SetValue(this.Value, this.ValuePriority); - } - } + PriorityLevel level; - /// - /// Throws an exception if is invalid. - /// - /// The value. - 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); + } } } /// - /// Called when a binding's value changes. + /// Gets the with the specified priority, creating it if it + /// doesn't already exist. /// - /// The changed entry. - private void EntryChanged(BindingEntry changed) + /// The priority. + /// The priority level. + 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); } - } - /// - /// Called when a binding completes. - /// - /// The completed entry. - private void EntryCompleted(BindingEntry entry) - { - this.Remove(entry); + return result; } /// - /// Sets the current value and notifies all observers. + /// Updates the current and notifies all subscibers. /// - /// The new value. - /// The priority of the binding which produced the value. - private void SetValue(object value, int priority) + /// The value to set. + /// The priority level that the value came from. + 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.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)); } /// - /// Removes the specified binding entry and updates the current value. - /// - /// The binding entry to remove. - private void Remove(BindingEntry entry) - { - entry.Dispose(); - this.bindings.Remove(entry); - this.UpdateValue(); - } - - /// - /// Updates the current value. + /// Throws an exception if is invalid. /// - private void UpdateValue() + /// The value. + 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); } /// - /// A registered binding. + /// Called when the value for a priority level changes. /// - public class BindingEntry : IDisposable + /// The changed entry. + private void ValueChanged(PriorityLevel level) { - /// - /// The binding subscription. - /// - private IDisposable subscription; - - /// - /// Gets a description of the binding. - /// - public string Description + if (level.Priority <= this.ValuePriority) { - get; - private set; - } - - /// - /// The priority of the binding. - /// - public int Priority - { - get; - private set; - } - - /// - /// The current value of the binding. - /// - public object Value - { - get; - private set; - } - - /// - /// Starts listening to the specified binding. - /// - /// The binding. - /// The binding priority. - /// Called when the binding changes. - /// Called when the binding completes. - public void Start( - IObservable binding, - int priority, - Action changed, - Action completed) - { - Contract.Requires(binding != null); - Contract.Requires(changed != null); - Contract.Requires(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)); - } - - /// - /// Ends the binding subscription. - /// - 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); } } } diff --git a/Perspex.Themes.Default/TabControlStyle.cs b/Perspex.Themes.Default/TabControlStyle.cs index efd112128b..fb8c8b78a8 100644 --- a/Perspex.Themes.Default/TabControlStyle.cs +++ b/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 { diff --git a/Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs b/Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs index f2f53991c3..b3c4d90f84 100644 --- a/Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs +++ b/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(() => + { + 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(() => - { - 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(); - 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] diff --git a/Tests/Perspex.Base.UnitTests/PriorityValueTests.cs b/Tests/Perspex.Base.UnitTests/PriorityValueTests.cs index ac5bf54f7a..48ec7a27a8 100644 --- a/Tests/Perspex.Base.UnitTests/PriorityValueTests.cs +++ b/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("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("na"); + var source = new BehaviorSubject("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("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("bar"); + var source = new BehaviorSubject("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("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(); + + 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(); + + 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); + } + /// /// Returns an observable that returns a single value but does not complete. ///