diff --git a/Perspex.UnitTests/Controls/ContentControlTests.cs b/Perspex.UnitTests/Controls/ContentControlTests.cs index 4a9cf0b24a..5c42fc3a5a 100644 --- a/Perspex.UnitTests/Controls/ContentControlTests.cs +++ b/Perspex.UnitTests/Controls/ContentControlTests.cs @@ -71,7 +71,10 @@ namespace Perspex.UnitTests.Controls Border border = new Border(); border.Background = new Perspex.Media.SolidColorBrush(0xffffffff); ContentPresenter contentPresenter = new ContentPresenter(); - contentPresenter.SetValue(ContentPresenter.ContentProperty, parent.GetObservable(ContentControl.ContentProperty)); + contentPresenter.Bind( + ContentPresenter.ContentProperty, + parent.GetObservable(ContentControl.ContentProperty), + BindingPriority.Template); border.Content = contentPresenter; return border; }); diff --git a/Perspex.UnitTests/Perspex.UnitTests.csproj b/Perspex.UnitTests/Perspex.UnitTests.csproj index aecd2461e4..7e88ba4225 100644 --- a/Perspex.UnitTests/Perspex.UnitTests.csproj +++ b/Perspex.UnitTests/Perspex.UnitTests.csproj @@ -71,6 +71,7 @@ + diff --git a/Perspex.UnitTests/PerspexObjectTests.cs b/Perspex.UnitTests/PerspexObjectTests.cs index 93e286a727..54088d66cb 100644 --- a/Perspex.UnitTests/PerspexObjectTests.cs +++ b/Perspex.UnitTests/PerspexObjectTests.cs @@ -249,7 +249,7 @@ namespace Perspex.UnitTests Class1 source = new Class1(); source.SetValue(Class1.FooProperty, "initial"); - target.SetValue(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); + target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); Assert.AreEqual("initial", target.GetValue(Class1.FooProperty)); } @@ -273,7 +273,7 @@ namespace Perspex.UnitTests Class1 source = new Class1(); source.SetValue(Class1.FooProperty, "initial"); - target.SetValue(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); + target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); source.SetValue(Class1.FooProperty, "subsequent"); Assert.AreEqual("subsequent", target.GetValue(Class1.FooProperty)); @@ -286,7 +286,7 @@ namespace Perspex.UnitTests Class1 source = new Class1(); source.SetValue(Class1.FooProperty, "initial"); - target.SetValue(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); + target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); target.ClearValue(Class1.FooProperty); source.SetValue(Class1.FooProperty, "newvalue"); @@ -300,7 +300,7 @@ namespace Perspex.UnitTests Class1 source = new Class1(); source.SetValue(Class1.FooProperty, "initial"); - target.SetValue(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); + target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); target.SetValue(Class1.FooProperty, "reset"); source.SetValue(Class1.FooProperty, "newvalue"); @@ -321,87 +321,93 @@ namespace Perspex.UnitTests [TestMethod] public void StyleBinding_Overrides_Default_Value() { - Class1 target = new Class1(); + throw new NotImplementedException(); + //Class1 target = new Class1(); - target.SetValue(Class1.FooProperty, "stylevalue", Observable.Return(true)); + //target.Bind(Class1.FooProperty, "stylevalue", Observable.Return(true)); - Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); + //Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); } [TestMethod] public void StyleBinding_Doesnt_Override_Local_Value() { - Class1 target = new Class1(); + throw new NotImplementedException(); + //Class1 target = new Class1(); - target.SetValue(Class1.FooProperty, "newvalue"); - target.SetValue(Class1.FooProperty, "stylevalue", Observable.Return(true)); + //target.SetValue(Class1.FooProperty, "newvalue"); + //target.SetValue(Class1.FooProperty, "stylevalue", Observable.Return(true)); - Assert.AreEqual("newvalue", target.GetValue(Class1.FooProperty)); + //Assert.AreEqual("newvalue", target.GetValue(Class1.FooProperty)); } [TestMethod] public void StyleBinding_Deactivated_Doesnt_Override_Default_Value() { - Class1 target = new Class1(); + throw new NotImplementedException(); + //Class1 target = new Class1(); - target.SetValue(Class1.FooProperty, "stylevalue", Observable.Return(false)); + //target.SetValue(Class1.FooProperty, "stylevalue", Observable.Return(false)); - Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + //Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); } [TestMethod] public void StyleBinding_Toggles_On_Activation() { - Class1 target = new Class1(); + throw new NotImplementedException(); + //Class1 target = new Class1(); - Subject source = new Subject(); - target.SetValue(Class1.FooProperty, "stylevalue", source); + //Subject source = new Subject(); + //target.SetValue(Class1.FooProperty, "stylevalue", source); - Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); - source.OnNext(true); - Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); - source.OnNext(false); - Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + //Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + //source.OnNext(true); + //Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); + //source.OnNext(false); + //Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); } [TestMethod] public void StyleBinding_Detaches_OnCompleted() { - Class1 target = new Class1(); + throw new NotImplementedException(); + //Class1 target = new Class1(); - Subject source = new Subject(); - target.SetValue(Class1.FooProperty, "stylevalue", source); + //Subject source = new Subject(); + //target.SetValue(Class1.FooProperty, "stylevalue", source); - Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); - source.OnNext(true); - Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); - source.OnCompleted(); - Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + //Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + //source.OnNext(true); + //Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); + //source.OnCompleted(); + //Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); } [TestMethod] public void Later_StyleBindings_Have_Precedence() { - Class1 target = new Class1(); - - Subject source1 = new Subject(); - Subject source2 = new Subject(); - target.SetValue(Class1.FooProperty, "style1", source1); - target.SetValue(Class1.FooProperty, "style2", source2); - - Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); - source1.OnNext(true); - Assert.AreEqual("style1", target.GetValue(Class1.FooProperty)); - source2.OnNext(true); - Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); - source1.OnNext(false); - Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); - source2.OnNext(false); - Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); - source2.OnNext(true); - Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); - source1.OnNext(true); - Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); + throw new NotImplementedException(); + //Class1 target = new Class1(); + + //Subject source1 = new Subject(); + //Subject source2 = new Subject(); + //target.SetValue(Class1.FooProperty, "style1", source1); + //target.SetValue(Class1.FooProperty, "style2", source2); + + //Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + //source1.OnNext(true); + //Assert.AreEqual("style1", target.GetValue(Class1.FooProperty)); + //source2.OnNext(true); + //Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); + //source1.OnNext(false); + //Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); + //source2.OnNext(false); + //Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + //source2.OnNext(true); + //Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); + //source1.OnNext(true); + //Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); } private class Class1 : PerspexObject diff --git a/Perspex.UnitTests/PriorityValueTEsts.cs b/Perspex.UnitTests/PriorityValueTEsts.cs new file mode 100644 index 0000000000..f198161691 --- /dev/null +++ b/Perspex.UnitTests/PriorityValueTEsts.cs @@ -0,0 +1,201 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.UnitTests +{ + using System; + using System.Linq; + using System.Reactive.Disposables; + using System.Reactive.Linq; + using System.Reactive.Subjects; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class PriorityValueTests + { + [TestMethod] + public void Initial_Value_Should_Be_UnsetValue() + { + var target = new PriorityValue(); + + Assert.AreSame(PerspexProperty.UnsetValue, target.Value); + } + + [TestMethod] + public void First_Binding_Sets_Value() + { + var target = new PriorityValue(); + + target.Add(this.Single("foo"), 0); + + Assert.AreEqual("foo", target.Value); + } + + [TestMethod] + public void Changing_Binding_Should_Set_Value() + { + var target = new PriorityValue(); + var subject = new BehaviorSubject("foo"); + + target.Add(subject, 0); + Assert.AreEqual("foo", target.Value); + subject.OnNext("bar"); + Assert.AreEqual("bar", target.Value); + } + + [TestMethod] + public void Binding_With_Lower_Priority_Has_Precedence() + { + var target = new PriorityValue(); + + target.Add(this.Single("foo"), 1); + target.Add(this.Single("bar"), 0); + target.Add(this.Single("baz"), 1); + + Assert.AreEqual("bar", target.Value); + } + + [TestMethod] + public void Later_Binding_With_Same_Priority_Should_Take_Precedence() + { + var target = new PriorityValue(); + + target.Add(this.Single("foo"), 1); + target.Add(this.Single("bar"), 0); + target.Add(this.Single("baz"), 0); + target.Add(this.Single("qux"), 1); + + Assert.AreEqual("baz", target.Value); + } + + [TestMethod] + public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value() + { + var target = new PriorityValue(); + var subject = new BehaviorSubject("bar"); + + target.Add(this.Single("foo"), 0); + target.Add(subject, 1); + Assert.AreEqual("foo", target.Value); + subject.OnNext("baz"); + Assert.AreEqual("foo", target.Value); + } + + [TestMethod] + public void UnsetValue_Should_Fall_Back_To_Next_Binding() + { + var target = new PriorityValue(); + var subject = new BehaviorSubject("bar"); + + target.Add(subject, 0); + target.Add(this.Single("foo"), 1); + + Assert.AreEqual("bar", target.Value); + + subject.OnNext(PerspexProperty.UnsetValue); + + Assert.AreEqual("foo", target.Value); + } + + [TestMethod] + public void Adding_Value_Should_Call_OnNext() + { + var target = new PriorityValue(); + bool called = false; + + target.Subscribe(value => called = (value.Item1 == PerspexProperty.UnsetValue && (string)value.Item2 == "foo")); + target.Add(this.Single("foo"), 0); + + Assert.IsTrue(called); + } + + [TestMethod] + public void Changing_Value_Should_Call_OnNext() + { + var target = new PriorityValue(); + var subject = new BehaviorSubject("foo"); + bool called = false; + + target.Add(subject, 0); + target.Subscribe(value => called = ((string)value.Item1 == "foo" && (string)value.Item2 == "bar")); + subject.OnNext("bar"); + + Assert.IsTrue(called); + } + + [TestMethod] + public void Disposing_A_Binding_Should_Revert_To_Next_Value() + { + var target = new PriorityValue(); + + target.Add(this.Single("foo"), 0); + var disposable = target.Add(this.Single("bar"), 0); + + Assert.AreEqual("bar", target.Value); + disposable.Dispose(); + Assert.AreEqual("foo", target.Value); + } + + [TestMethod] + public void Disposing_A_Binding_Should_Remove_BindingEntry() + { + var target = new PriorityValue(); + + target.Add(this.Single("foo"), 0); + var disposable = target.Add(this.Single("bar"), 0); + + Assert.AreEqual(2, target.GetBindings().Count()); + disposable.Dispose(); + Assert.AreEqual(1, target.GetBindings().Count()); + } + + [TestMethod] + public void Completing_A_Binding_Should_Revert_To_Next_Value() + { + var target = new PriorityValue(); + var subject = new BehaviorSubject("bar"); + + target.Add(this.Single("foo"), 0); + target.Add(subject, 0); + + Assert.AreEqual("bar", target.Value); + subject.OnCompleted(); + Assert.AreEqual("foo", target.Value); + } + + [TestMethod] + public void Completing_A_Binding_Should_Remove_BindingEntry() + { + var target = new PriorityValue(); + var subject = new BehaviorSubject("bar"); + + target.Add(this.Single("foo"), 0); + target.Add(subject, 0); + + Assert.AreEqual(2, target.GetBindings().Count()); + subject.OnCompleted(); + Assert.AreEqual(1, target.GetBindings().Count()); + } + + /// + /// Returns an observable that returns a single value but does not complete. + /// + /// The type of the observable. + /// The value. + /// The observable. + /// + /// Seems like there should be something that does this in Rx but I couldn't find it. + /// + private IObservable Single(T value) + { + return Observable.Create(observer => + { + observer.OnNext(value); + return Disposable.Create(() => { }); + }); + } + } +} diff --git a/Perspex.UnitTests/Styling/ActivatorTests.cs b/Perspex.UnitTests/Styling/ActivatorTests.cs index fa96a9abd5..14e9ab0c94 100644 --- a/Perspex.UnitTests/Styling/ActivatorTests.cs +++ b/Perspex.UnitTests/Styling/ActivatorTests.cs @@ -18,7 +18,7 @@ public void Activator_And_Should_Follow_Single_Input() { var inputs = new[] { new TestSubject(false) }; - var target = new Activator(inputs, "", ActivatorMode.And); + var target = new Activator(inputs, ActivatorMode.And); var result = new TestObserver(); target.Subscribe(result); @@ -42,7 +42,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, "", ActivatorMode.And); + var target = new Activator(inputs, ActivatorMode.And); var result = new TestObserver(); target.Subscribe(result); @@ -67,7 +67,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, "", ActivatorMode.And); + var target = new Activator(inputs, ActivatorMode.And); var result = new TestObserver(); target.Subscribe(result); @@ -93,7 +93,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, "", ActivatorMode.And); + var target = new Activator(inputs, ActivatorMode.And); var result = new TestObserver(); target.Subscribe(result); @@ -110,7 +110,7 @@ public void Activator_Or_Should_Follow_Single_Input() { var inputs = new[] { new TestSubject(false) }; - var target = new Activator(inputs, "", ActivatorMode.Or); + var target = new Activator(inputs, ActivatorMode.Or); var result = new TestObserver(); target.Subscribe(result); @@ -134,7 +134,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, "", ActivatorMode.Or); + var target = new Activator(inputs, ActivatorMode.Or); var result = new TestObserver(); target.Subscribe(result); @@ -158,7 +158,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, "", ActivatorMode.Or); + var target = new Activator(inputs, ActivatorMode.Or); var result = new TestObserver(); target.Subscribe(result); @@ -183,7 +183,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, "", ActivatorMode.Or); + var target = new Activator(inputs, ActivatorMode.Or); var result = new TestObserver(); target.Subscribe(result); diff --git a/Perspex.UnitTests/Styling/TestControlBase.cs b/Perspex.UnitTests/Styling/TestControlBase.cs index 3a9611cb4a..8e274ca1de 100644 --- a/Perspex.UnitTests/Styling/TestControlBase.cs +++ b/Perspex.UnitTests/Styling/TestControlBase.cs @@ -30,8 +30,9 @@ namespace Perspex.UnitTests.Styling set; } - public virtual void SetValue(PerspexProperty property, object value, IObservable activator) + public void Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { + throw new NotImplementedException(); } } } diff --git a/Perspex.UnitTests/Styling/TestTemplatedControl.cs b/Perspex.UnitTests/Styling/TestTemplatedControl.cs index e5998e761d..fde5f3fa3f 100644 --- a/Perspex.UnitTests/Styling/TestTemplatedControl.cs +++ b/Perspex.UnitTests/Styling/TestTemplatedControl.cs @@ -38,6 +38,9 @@ namespace Perspex.UnitTests.Styling throw new NotImplementedException(); } - public abstract void SetValue(PerspexProperty property, object value, System.IObservable activator); + public void Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } } } diff --git a/Perspex.Windows/Window.cs b/Perspex.Windows/Window.cs index 6d2cffdaaf..d1531386e1 100644 --- a/Perspex.Windows/Window.cs +++ b/Perspex.Windows/Window.cs @@ -85,7 +85,10 @@ namespace Perspex.Windows Border border = new Border(); border.Background = new Perspex.Media.SolidColorBrush(0xffffffff); ContentPresenter contentPresenter = new ContentPresenter(); - contentPresenter.SetValue(ContentPresenter.ContentProperty, this.GetObservable(Window.ContentProperty)); + contentPresenter.Bind( + ContentPresenter.ContentProperty, + this.GetObservable(Window.ContentProperty), + BindingPriority.Style); border.Content = contentPresenter; return border; } diff --git a/Perspex/Perspex.csproj b/Perspex/Perspex.csproj index b3f86487de..9d33cbd892 100644 --- a/Perspex/Perspex.csproj +++ b/Perspex/Perspex.csproj @@ -69,6 +69,7 @@ + diff --git a/Perspex/PerspexObject.cs b/Perspex/PerspexObject.cs index 5c97cbc440..4598eb03f8 100644 --- a/Perspex/PerspexObject.cs +++ b/Perspex/PerspexObject.cs @@ -16,6 +16,27 @@ namespace Perspex using System.Reflection; using Splat; + /// + /// The priority of a binding. + /// + public enum BindingPriority + { + /// + /// A local value. + /// + LocalValue, + + /// + /// A style binding. + /// + Style, + + /// + /// A template binding. + /// + Template, + } + /// /// An object with support. /// @@ -99,15 +120,16 @@ namespace Perspex /// public void BeginDeferChanges() { - foreach (PriorityValue v in this.values.Values) - { - v.BeginDeferChanges(); - } - - this.Log().Debug(string.Format( - "Defer style changes on {0} (#{1:x8})", - this.GetType().Name, - this.GetHashCode())); + throw new NotImplementedException(); + //foreach (PriorityValue v in this.values.Values) + //{ + // v.BeginDeferChanges(); + //} + + //this.Log().Debug(string.Format( + // "Defer style changes on {0} (#{1:x8})", + // this.GetType().Name, + // this.GetHashCode())); } /// @@ -116,15 +138,16 @@ namespace Perspex /// public void EndDeferChanges() { - foreach (PriorityValue v in this.values.Values) - { - v.EndDeferChanges(); - } - - this.Log().Debug(string.Format( - "End defer style changes on {0} (#{1:x8})", - this.GetType().Name, - this.GetHashCode())); + throw new NotImplementedException(); + //foreach (PriorityValue v in this.values.Values) + //{ + // v.EndDeferChanges(); + //} + + //this.Log().Debug(string.Format( + // "End defer style changes on {0} (#{1:x8})", + // this.GetType().Name, + // this.GetHashCode())); } /// @@ -189,19 +212,20 @@ namespace Perspex /// The property. public void ClearBinding(PerspexProperty property) { - Contract.Requires(property != null); - PriorityValue value; - - if (this.values.TryGetValue(property, out value)) - { - value.ClearLocalBinding(); - - this.Log().Debug(string.Format( - "Cleared binding on {0}.{1} (#{2:x8})", - this.GetType().Name, - property.Name, - this.GetHashCode())); - } + throw new NotImplementedException(); + //Contract.Requires(property != null); + //PriorityValue value; + + //if (this.values.TryGetValue(property, out value)) + //{ + // value.ClearLocalBinding(); + + // this.Log().Debug(string.Format( + // "Cleared binding on {0}.{1} (#{2:x8})", + // this.GetType().Name, + // property.Name, + // this.GetHashCode())); + //} } /// @@ -312,7 +336,7 @@ namespace Perspex if (this.values.TryGetValue(property, out value)) { - result = value.GetEffectiveValue(); + result = value.Value; } else { @@ -364,32 +388,6 @@ namespace Perspex return this.values.ContainsKey(property); } - /// - /// Sets a value. - /// - /// The type of the property. - /// The property. - /// The value. - public void SetValue(PerspexProperty property, T value) - { - Contract.Requires(property != null); - - this.SetValue((PerspexProperty)property, value); - } - - /// - /// Binds a to an observable. - /// - /// The type of the property. - /// The property. - /// The observable. - public void SetValue(PerspexProperty property, IObservable source) - { - Contract.Requires(property != null); - - this.SetValue((PerspexProperty)property, source); - } - /// /// Sets a value. /// @@ -399,8 +397,6 @@ namespace Perspex { Contract.Requires(property != null); - IObservable binding = TryCastToObservable(value); - PriorityValue v; if (!this.values.TryGetValue(property, out v)) @@ -414,57 +410,59 @@ namespace Perspex this.values.Add(property, v); } - if (binding == null) - { - v.SetLocalValue(value); - } - else - { - v.SetLocalBinding(binding); - - this.Log().Debug(string.Format( - "Bound value of {0}.{1} (#{2:x8})", - this.GetType().Name, - property.Name, - this.GetHashCode())); - } + throw new NotImplementedException(); + //v.SetLocalValue(value); } /// - /// Binds a to a style. + /// Sets a value. /// + /// The type of the property. /// The property. - /// The activated value. - /// An observable which activates the value. - /// - /// Style bindings have a lower precedence than local value bindings. They are toggled - /// on or off by and can be unbound by the activator - /// completing. - /// - public void SetValue(PerspexProperty property, object value, IObservable activator) + /// The value. + public void SetValue(PerspexProperty property, T value) { Contract.Requires(property != null); - Contract.Requires(activator != null); - - PriorityValue v; - - if (!this.values.TryGetValue(property, out v)) - { - v = this.CreatePriorityValue(property); - this.values.Add(property, v); - } - v.AddStyle(activator, value); + this.SetValue((PerspexProperty)property, value); + } - IObservableDescription description = activator as IObservableDescription; - string bindingDescription = description != null ? description.Description : "[unnamed]"; + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// The priority of the binding. + public void Bind( + PerspexProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); this.Log().Debug(string.Format( - "Bound value of {0}.{1} (#{2:x8}) to style '{3}'", + "Bound value of {0}.{1} (#{2:x8})", this.GetType().Name, property.Name, - this.GetHashCode(), - bindingDescription)); + this.GetHashCode())); + } + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// The priority of the binding. + public void Bind( + PerspexProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + Contract.Requires(property != null); + + this.Bind((PerspexProperty)property, (IObservable)source, priority); } private static IObservable BoxObservable(IObservable observable) @@ -478,34 +476,6 @@ namespace Perspex }); } - private static IObservable TryCastToObservable(object value) - { - Type observableType = value.GetType().GetTypeInfo() - .ImplementedInterfaces - .FirstOrDefault(x => - x.IsConstructedGenericType && - x.GetGenericTypeDefinition() == typeof(IObservable<>)); - - IObservable result = null; - - if (observableType != null) - { - result = value as IObservable; - - if (result == null) - { - MethodInfo cast = typeof(PerspexObject).GetTypeInfo() - .DeclaredMethods - .FirstOrDefault(x => x.Name == "BoxObservable") - .MakeGenericMethod(observableType.GenericTypeArguments[0]); - - result = (IObservable)cast.Invoke(null, new[] { value }); - } - } - - return result; - } - private PriorityValue CreatePriorityValue(PerspexProperty property) { PriorityValue result = new PriorityValue(); diff --git a/Perspex/PriorityValue.cs b/Perspex/PriorityValue.cs index f621b55f12..17eb9c6162 100644 --- a/Perspex/PriorityValue.cs +++ b/Perspex/PriorityValue.cs @@ -11,184 +11,262 @@ namespace Perspex using System.Linq; using System.Reactive.Disposables; - internal class PriorityValue : IObservable> + /// + /// Maintains a list of prioritised bindings together with a current value. + /// + public class PriorityValue : IObservable> { - private object localValue = PerspexProperty.UnsetValue; - - private IDisposable localBinding; - - private object lastValue = PerspexProperty.UnsetValue; - - private List styles = new List(); - + /// + /// The currently registered binding entries. + /// + private LinkedList bindings = new LinkedList(); + + /// + /// The current observers. + /// private List>> observers = new List>>(); - private int defer; + /// + /// The current value. + /// + private object value; - private bool dirty; + /// + /// The priority of the binding that is currently active. + /// + private int valuePriority = int.MaxValue; - public object LocalValue + /// + /// Initializes a new instance of the class. + /// + public PriorityValue() { - get - { - return this.localValue; - } - - set - { - if (!object.Equals(this.localValue, value)) - { - this.localValue = value; - this.Push(); - } - } + this.value = PerspexProperty.UnsetValue; } - public void ClearLocalBinding() + /// + /// Gets the currently active bindings on this object. + /// + /// An enumerable collection of bindings. + public IEnumerable GetBindings() { - if (this.localBinding != null) - { - this.localBinding.Dispose(); - } + return this.bindings; } - public void SetLocalValue(object value) + /// + /// Gets the current value. + /// + public object Value { - if (this.localBinding != null) - { - this.localBinding.Dispose(); - } - - this.LocalValue = value; + get { return this.value; } } - public void SetLocalBinding(IObservable binding) + /// + /// Adds a new binding. + /// + /// The binding. + /// The binding priority. + /// + /// A disposable that will remove the binding. + /// + public IDisposable Add(IObservable binding, int priority) { - if (this.localBinding != null) - { - this.localBinding.Dispose(); - } + BindingEntry entry = new BindingEntry(); + LinkedListNode insert = this.bindings.First; - this.localBinding = binding.Subscribe(value => this.LocalValue = value); - } - - public void AddStyle(IObservable activator, object value) - { - Contract.Requires(activator != null); - - StyleEntry entry = new StyleEntry(activator, value, this.Push, e => this.styles.Remove(e)); - - this.styles.Add(entry); - - if (this.localValue == PerspexProperty.UnsetValue) + while (insert != null && insert.Value.Priority < priority) { - this.Push(); + insert = insert.Next; } - } - public object GetEffectiveValue() - { - if (this.localValue != PerspexProperty.UnsetValue) + if (insert == null) { - return this.localValue; + this.bindings.AddLast(entry); } else { - foreach (StyleEntry style in Enumerable.Reverse(this.styles)) - { - if (style.Active) - { - return style.Value; - } - } + this.bindings.AddBefore(insert, entry); } - return PerspexProperty.UnsetValue; + entry.Start(binding, priority, this.EntryChanged, this.EntryCompleted); + + return Disposable.Create(() => + { + entry.Dispose(); + this.bindings.Remove(entry); + this.UpdateValue(); + }); } + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The object that is to receive notifications. + /// + /// A reference to an interface that allows observers to stop receiving notifications + /// before the provider has finished sending them. + /// public IDisposable Subscribe(IObserver> observer) { - Contract.Requires(observer != null); - this.observers.Add(observer); - return Disposable.Create(() => this.observers.Remove(observer)); } - public void BeginDeferChanges() + /// + /// Called when an binding's value changes. + /// + /// The changed entry. + private void EntryChanged(BindingEntry changed) { - if (this.defer++ == 0) + if (changed.Priority <= this.valuePriority) { - this.dirty = false; + this.UpdateValue(); } } - public void EndDeferChanges() + /// + /// Called when an binding completes. + /// + /// The completed entry. + private void EntryCompleted(BindingEntry entry) { - if (this.defer > 0 && --this.defer == 0 && dirty) - { - this.Push(); - } + entry.Dispose(); + this.bindings.Remove(entry); + this.UpdateValue(); } - private void Push() + /// + /// Notifies all observers of a change in value. + /// + /// The old and new values. + private void OnNext(Tuple value) { - if (defer == 0) + foreach (var observer in this.observers) { - object value = this.GetEffectiveValue(); + observer.OnNext(value); + } + } - if (!object.Equals(this.lastValue, value)) - { - foreach (IObserver> observer in this.observers) - { - observer.OnNext(Tuple.Create(this.lastValue, value)); - } + /// + /// Sets the current value and notifies all observers. + /// + /// The new value. + /// The priority of the binding which produced the value. + private void SetValue(object value, int priority) + { + object old = this.value; - this.lastValue = value; - } + this.valuePriority = priority; + + if (!EqualityComparer.Default.Equals(old, value)) + { + this.value = value; + this.OnNext(Tuple.Create(old, value)); } - else + } + + /// + /// Updates the current value. + /// + private void UpdateValue() + { + foreach (BindingEntry entry in this.bindings) { - dirty = true; + if (entry.Value != PerspexProperty.UnsetValue) + { + this.SetValue(entry.Value, entry.Priority); + return; + } } } - private class StyleEntry + /// + /// A registered binding. + /// + public class BindingEntry : IDisposable { - private IObservable activator; + /// + /// The binding subscription. + /// + private IDisposable subscription; + + /// + /// The priority of the binding. + /// + public int Priority + { + get; + private set; + } - public StyleEntry( - IObservable activator, - object value, - Action activeChanged, - Action completed) + /// + /// The current value of the binding. + /// + public object Value { - Contract.Requires(activator != null); - Contract.Requires(activeChanged != null); + get; + private set; + } - this.activator = activator; - this.Value = value; + /// + /// 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); - this.activator.Subscribe(x => + if (this.subscription != null) { - this.Active = x; - activeChanged(); - }, - () => completed(this)); - } + throw new Exception("PriorityValue.Entry.Start() called more than once."); + } + + this.Priority = priority; + this.Value = PerspexProperty.UnsetValue; - public bool Active - { - get; - private set; + if (binding is IObservableDescription) + { + this.Description = ((IObservableDescription)binding).Description; + } + + this.subscription = binding.Subscribe( + value => + { + this.Value = value; + changed(this); + }, + () => completed(this)); } - public object Value + /// + /// Gets a description of the binding. + /// + public string Description { get; private set; } + + /// + /// Ends the binding subscription. + /// + public void Dispose() + { + if (this.subscription != null) + { + this.subscription.Dispose(); + } + } } } } diff --git a/Perspex/Styling/IStyleable.cs b/Perspex/Styling/IStyleable.cs index 2dc2a983cd..79d7a65c58 100644 --- a/Perspex/Styling/IStyleable.cs +++ b/Perspex/Styling/IStyleable.cs @@ -30,16 +30,15 @@ namespace Perspex.Styling ITemplatedControl TemplatedParent { get; } /// - /// Binds a to a style. + /// Binds a to an observable. /// + /// The type of the property. /// The property. - /// The activated value. - /// An observable which activates the value. - /// - /// Style bindings have a lower precedence than local value bindings. They are toggled - /// on or off by and can be unbound by the activator - /// completing. - /// - void SetValue(PerspexProperty property, object value, IObservable activator); + /// The observable. + /// The priority of the binding. + void Bind( + PerspexProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue); } } diff --git a/Perspex/Styling/Selector.cs b/Perspex/Styling/Selector.cs index 516608c46c..fe44e99e7e 100644 --- a/Perspex/Styling/Selector.cs +++ b/Perspex/Styling/Selector.cs @@ -79,7 +79,7 @@ namespace Perspex.Styling selector = selector.MovePrevious(); } - return new StyleActivator(inputs, this.ToString()); + return new StyleActivator(inputs); } public override string ToString() diff --git a/Perspex/Styling/Selectors.cs b/Perspex/Styling/Selectors.cs index 935911fa97..2878b4e0ed 100644 --- a/Perspex/Styling/Selectors.cs +++ b/Perspex/Styling/Selectors.cs @@ -49,7 +49,6 @@ namespace Perspex.Styling return new StyleActivator( descendentMatches, - "Descendent", ActivatorMode.Or); }, }; diff --git a/Perspex/Styling/Style.cs b/Perspex/Styling/Style.cs index 0fc7e98415..2ca83ef4b0 100644 --- a/Perspex/Styling/Style.cs +++ b/Perspex/Styling/Style.cs @@ -47,7 +47,8 @@ namespace Perspex.Styling { foreach (Setter setter in this.Setters) { - control.SetValue(setter.Property, setter.Value, activator); + StyleBinding binding = new StyleBinding(activator, setter.Value, description); + control.Bind(setter.Property, binding, BindingPriority.Style); } } } diff --git a/Perspex/Styling/StyleActivator.cs b/Perspex/Styling/StyleActivator.cs index 124021fbb7..7c29abcd87 100644 --- a/Perspex/Styling/StyleActivator.cs +++ b/Perspex/Styling/StyleActivator.cs @@ -17,7 +17,7 @@ namespace Perspex.Styling Or, } - public class StyleActivator : IObservable, IObservableDescription + public class StyleActivator : IObservable { ActivatorMode mode; @@ -29,12 +29,10 @@ namespace Perspex.Styling public StyleActivator( IEnumerable> inputs, - string description, ActivatorMode mode = ActivatorMode.And) { int i = 0; - this.Description = description; this.mode = mode; foreach (IObservable input in inputs) @@ -58,12 +56,6 @@ namespace Perspex.Styling private set; } - public string Description - { - get; - private set; - } - public bool HasCompleted { get; diff --git a/Perspex/Styling/StyleBinding.cs b/Perspex/Styling/StyleBinding.cs new file mode 100644 index 0000000000..3aebf48195 --- /dev/null +++ b/Perspex/Styling/StyleBinding.cs @@ -0,0 +1,85 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 Tricycle. All rights reserved. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Styling +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reactive.Disposables; + using System.Reactive.Subjects; + using System.Text; + using System.Threading.Tasks; + + internal class StyleBinding : IObservable, IObservableDescription + { + private List> observers = new List>(); + + public StyleBinding( + StyleActivator activator, + object activatedValue, + string description) + { + this.Activator = activator; + this.ActivatedValue = activatedValue; + this.Description = description; + + this.Activator.Subscribe( + active => this.OnNext(active ? this.ActivatedValue : PerspexProperty.UnsetValue), + error => this.OnError(error), + () => this.OnCompleted()); + } + + public StyleActivator Activator + { + get; + private set; + } + + public string Description + { + get; + private set; + } + + public object ActivatedValue + { + get; + private set; + } + + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + this.observers.Add(observer); + return Disposable.Create(() => this.observers.Remove(observer)); + } + + private void OnCompleted() + { + foreach (var observer in this.observers) + { + observer.OnCompleted(); + } + } + + private void OnError(Exception error) + { + foreach (var observer in this.observers) + { + observer.OnError(error); + } + } + + private void OnNext(object value) + { + foreach (var observer in this.observers) + { + observer.OnNext(value); + } + } + } +} diff --git a/Perspex/Themes/Default/ButtonStyle.cs b/Perspex/Themes/Default/ButtonStyle.cs index 2d6e6d3ffd..7d42ae6e65 100644 --- a/Perspex/Themes/Default/ButtonStyle.cs +++ b/Perspex/Themes/Default/ButtonStyle.cs @@ -57,7 +57,10 @@ namespace Perspex.Themes.Default border.Id = "border"; border.Padding = new Thickness(3); ContentPresenter contentPresenter = new ContentPresenter(); - contentPresenter.SetValue(ContentPresenter.ContentProperty, control.GetObservable(Button.ContentProperty)); + contentPresenter.Bind( + ContentPresenter.ContentProperty, + control.GetObservable(Button.ContentProperty), + BindingPriority.Template); border.Content = contentPresenter; return border; } diff --git a/Perspex/tkfbyosx.jgd b/Perspex/tkfbyosx.jgd new file mode 100644 index 0000000000..f16387bfa3 --- /dev/null +++ b/Perspex/tkfbyosx.jgd @@ -0,0 +1,555 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 Tricycle. All rights reserved. +// +// ----------------------------------------------------------------------- + +namespace Perspex +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reactive; + using System.Reactive.Disposables; + using System.Reactive.Linq; + using System.Reflection; + using Splat; + + /// + /// The priority of a binding. + /// + public enum BindingPriority + { + /// + /// A local value. + /// + LocalValue, + + /// + /// A style binding. + /// + Style, + + /// + /// A template binding. + /// + Template, + } + + /// + /// An object with support. + /// + /// + /// This class is analogous to DependencyObject in WPF. + /// + public class PerspexObject : IEnableLogger + { + /// + /// The registered properties by type. + /// + private static Dictionary> registered = + new Dictionary>(); + + /// + /// The parent object that inherited values are inherited from. + /// + private PerspexObject inheritanceParent; + + /// + /// The set values/bindings on this object. + /// + private Dictionary values = + new Dictionary(); + + /// + /// Raised when a value changes on this object/ + /// + public event EventHandler PropertyChanged; + + /// + /// Gets or sets the parent object that inherited values + /// are inherited from. + /// + protected PerspexObject InheritanceParent + { + get + { + return this.inheritanceParent; + } + + set + { + if (this.inheritanceParent != value) + { + if (this.inheritanceParent != null) + { + this.inheritanceParent.PropertyChanged -= this.ParentPropertyChanged; + } + + var inherited = (from property in GetProperties(this.GetType()) + where property.Inherits + select new + { + Property = property, + Value = this.GetValue(property), + }).ToList(); + + this.inheritanceParent = value; + + foreach (var i in inherited) + { + object newValue = this.GetValue(i.Property); + + if (!object.Equals(i.Value, newValue)) + { + this.RaisePropertyChanged(i.Property, i.Value, newValue); + } + } + + if (this.inheritanceParent != null) + { + this.inheritanceParent.PropertyChanged += this.ParentPropertyChanged; + } + } + } + } + + /// + /// Defers property change notifications until is called. + /// + public void BeginDeferChanges() + { + throw new NotImplementedException(); + //foreach (PriorityValue v in this.values.Values) + //{ + // v.BeginDeferChanges(); + //} + + //this.Log().Debug(string.Format( + // "Defer style changes on {0} (#{1:x8})", + // this.GetType().Name, + // this.GetHashCode())); + } + + /// + /// Ends the defer of property change notifications initiated by a previous call + /// to . + /// + public void EndDeferChanges() + { + //foreach (PriorityValue v in this.values.Values) + //{ + // v.EndDeferChanges(); + //} + + //this.Log().Debug(string.Format( + // "End defer style changes on {0} (#{1:x8})", + // this.GetType().Name, + // this.GetHashCode())); + } + + /// + /// Gets all s registered on a type. + /// + /// The type. + /// A collection of definitions. + public static IEnumerable GetProperties(Type type) + { + Contract.Requires(type != null); + + TypeInfo i = type.GetTypeInfo(); + + while (type != null) + { + List list; + + if (registered.TryGetValue(type, out list)) + { + foreach (PerspexProperty p in list) + { + yield return p; + } + } + + type = type.GetTypeInfo().BaseType; + } + } + + /// + /// Registers a on a type. + /// + /// The type. + /// The property. + /// + /// You won't usually want to call this method directly, instead use the + /// method. + /// + public static void Register(Type type, PerspexProperty property) + { + Contract.Requires(type != null); + Contract.Requires(property != null); + + List list; + + if (!registered.TryGetValue(type, out list)) + { + list = new List(); + registered.Add(type, list); + } + + if (!list.Contains(property)) + { + list.Add(property); + } + } + + /// + /// Clears a binding on a , leaving the last bound value in + /// place. + /// + /// The property. + public void ClearBinding(PerspexProperty property) + { + throw new NotImplementedException(); + //Contract.Requires(property != null); + //PriorityValue value; + + //if (this.values.TryGetValue(property, out value)) + //{ + // value.ClearLocalBinding(); + + // this.Log().Debug(string.Format( + // "Cleared binding on {0}.{1} (#{2:x8})", + // this.GetType().Name, + // property.Name, + // this.GetHashCode())); + //} + } + + /// + /// Clears a value, including its binding. + /// + /// The property. + public void ClearValue(PerspexProperty property) + { + Contract.Requires(property != null); + + this.SetValue(property, PerspexProperty.UnsetValue); + } + + /// + /// Gets an observable for a . + /// + /// + /// + public IObservable GetObservable(PerspexProperty property) + { + Contract.Requires(property != null); + + return Observable.Create(observer => + { + EventHandler handler = (s, e) => + { + if (e.Property == property) + { + observer.OnNext(e.NewValue); + } + }; + + this.PropertyChanged += handler; + observer.OnNext(this.GetValue(property)); + + return () => + { + this.PropertyChanged -= handler; + }; + }); + } + + /// + /// Gets an observable for a . + /// + /// + /// + /// + public IObservable GetObservable(PerspexProperty property) + { + Contract.Requires(property != null); + + return this.GetObservable((PerspexProperty)property).Cast(); + } + + /// + /// Gets an observable for a . + /// + /// + /// + /// + public IObservable GetObservable(ReadOnlyPerspexProperty property) + { + Contract.Requires(property != null); + + return this.GetObservable((PerspexProperty)property.Property); + } + + /// + /// Gets an observable for a . + /// + /// + /// + /// + public IObservable> GetObservableWithHistory(PerspexProperty property) + { + return Observable.Create>(observer => + { + EventHandler handler = (s, e) => + { + if (e.Property == property) + { + observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue)); + } + }; + + this.PropertyChanged += handler; + + return () => + { + this.PropertyChanged -= handler; + }; + }); + } + + /// + /// Gets a value. + /// + /// The property. + /// The value. + public object GetValue(PerspexProperty property) + { + Contract.Requires(property != null); + + object result; + + PriorityValue value; + + if (this.values.TryGetValue(property, out value)) + { + result = value.Value; + } + else + { + result = PerspexProperty.UnsetValue; + } + + if (result == PerspexProperty.UnsetValue) + { + result = this.GetDefaultValue(property); + } + + return result; + } + + /// + /// Gets a value. + /// + /// The property. + /// The value. + public T GetValue(PerspexProperty property) + { + Contract.Requires(property != null); + + return (T)this.GetValue((PerspexProperty)property); + } + + /// + /// Gets a value. + /// + /// The type of the property. + /// The property. + /// The value. + public T GetValue(ReadOnlyPerspexProperty property) + { + Contract.Requires(property != null); + + return (T)this.GetValue(property.Property); + } + + /// + /// Checks whether a is set on this object. + /// + /// + /// + public bool IsSet(PerspexProperty property) + { + Contract.Requires(property != null); + + return this.values.ContainsKey(property); + } + + /// + /// Sets a value. + /// + /// The property. + /// The value. + public void SetValue(PerspexProperty property, object value) + { + Contract.Requires(property != null); + + PriorityValue v; + + if (!this.values.TryGetValue(property, out v)) + { + if (value == PerspexProperty.UnsetValue) + { + return; + } + + v = this.CreatePriorityValue(property); + this.values.Add(property, v); + } + + throw new NotImplementedException(); + //v.SetLocalValue(value); + } + + /// + /// Sets a value. + /// + /// The type of the property. + /// The property. + /// The value. + public void SetValue(PerspexProperty property, T value) + { + Contract.Requires(property != null); + + this.SetValue((PerspexProperty)property, value); + } + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// The priority of the binding. + public void Bind( + PerspexProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + + this.Log().Debug(string.Format( + "Bound value of {0}.{1} (#{2:x8})", + this.GetType().Name, + property.Name, + this.GetHashCode())); + } + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// The priority of the binding. + public void Bind( + PerspexProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + Contract.Requires(property != null); + + this.Bind((PerspexProperty)property, (IObservable)source, priority); + } + + private static IObservable BoxObservable(IObservable observable) + { + return Observable.Create(observer => + { + return observable.Subscribe(value => + { + observer.OnNext(value); + }); + }); + } + + private PriorityValue CreatePriorityValue(PerspexProperty property) + { + PriorityValue result = new PriorityValue(); + + result.Subscribe(x => + { + object oldValue = (x.Item1 == PerspexProperty.UnsetValue) ? + this.GetDefaultValue(property) : + x.Item1; + object newValue = (x.Item2 == PerspexProperty.UnsetValue) ? + this.GetDefaultValue(property) : + x.Item2; + + if (!object.Equals(oldValue, newValue)) + { + this.RaisePropertyChanged(property, oldValue, newValue); + + this.Log().Debug(string.Format( + "Set value of {0}.{1} (#{2:x8}) to {3}", + this.GetType().Name, + property.Name, + this.GetHashCode(), + newValue)); + } + }); + + return result; + } + + private object GetDefaultValue(PerspexProperty property) + { + if (property.Inherits && this.inheritanceParent != null) + { + return this.inheritanceParent.GetValue(property); + } + else + { + return property.GetDefaultValue(this.GetType()); + } + } + + /// + /// Called when a property is changed on the current . + /// + /// The event sender. + /// The event args. + /// + /// Checks for changes in an inherited property value. + /// + private void ParentPropertyChanged(object sender, PerspexPropertyChangedEventArgs e) + { + Contract.Requires(e != null); + + if (e.Property.Inherits && !this.IsSet(e.Property)) + { + this.RaisePropertyChanged(e.Property, e.OldValue, e.NewValue); + } + } + + /// + /// Raises the event. + /// + /// The property that has changed. + /// The old property value. + /// The new property value. + private void RaisePropertyChanged(PerspexProperty property, object oldValue, object newValue) + { + Contract.Requires(property != null); + + if (this.PropertyChanged != null) + { + this.PropertyChanged( + this, + new PerspexPropertyChangedEventArgs(property, oldValue, newValue)); + } + } + } +}