diff --git a/Perspex.UnitTests/PerspexObjectTests.cs b/Perspex.UnitTests/PerspexObjectTests.cs index 51a9720062..ebd5b14636 100644 --- a/Perspex.UnitTests/PerspexObjectTests.cs +++ b/Perspex.UnitTests/PerspexObjectTests.cs @@ -353,6 +353,37 @@ namespace Perspex.UnitTests target.Bind((PerspexProperty)Class1.FooProperty, Observable.Return((object)123)); } + [TestMethod] + public void BindTwoWay_Gets_Initial_Value_From_Source() + { + Class1 source = new Class1(); + Class1 target = new Class1(); + + source.SetValue(Class1.FooProperty, "initial"); + target.BindTwoWay(Class1.FooProperty, source, Class1.FooProperty); + + Assert.AreEqual("initial", target.GetValue(Class1.FooProperty)); + } + + [TestMethod] + public void BindTwoWay_Updates_Values() + { + Class1 source = new Class1(); + Class1 target = new Class1(); + + System.Diagnostics.Debug.WriteLine("source: " + source.GetHashCode()); + System.Diagnostics.Debug.WriteLine("target: " + target.GetHashCode()); + + source.SetValue(Class1.FooProperty, "first"); + target.BindTwoWay(Class1.FooProperty, source, Class1.FooProperty); + + Assert.AreEqual("first", target.GetValue(Class1.FooProperty)); + source.SetValue(Class1.FooProperty, "second"); + Assert.AreEqual("second", target.GetValue(Class1.FooProperty)); + target.SetValue(Class1.FooProperty, "third"); + Assert.AreEqual("third", source.GetValue(Class1.FooProperty)); + } + [TestMethod] public void Setting_UnsetValue_Reverts_To_Default_Value() { @@ -385,6 +416,79 @@ namespace Perspex.UnitTests Assert.AreEqual("newvalue", target.GetValue(Class1.FooProperty)); } + [TestMethod] + public void this_Operator_Returns_Value_Property() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + + Assert.AreEqual("newvalue", target[Class1.FooProperty]); + } + + [TestMethod] + public void this_Operator_Sets_Value_Property() + { + Class1 target = new Class1(); + + target[Class1.FooProperty] = "newvalue"; + + Assert.AreEqual("newvalue", target.GetValue(Class1.FooProperty)); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void this_Operator_Doesnt_Accept_Observable() + { + Class1 target = new Class1(); + + target[Class1.FooProperty] = Observable.Return("newvalue"); + } + + [TestMethod] + public void this_Operator_Binds_One_Way() + { + Class1 target1 = new Class1(); + Class1 target2 = new Class1(); + Binding binding = Class1.FooProperty.Bind().WithMode(BindingMode.OneWay); + + target1.SetValue(Class1.FooProperty, "first"); + target2[binding] = target1[!Class1.FooProperty]; + target1.SetValue(Class1.FooProperty, "second"); + + Assert.AreEqual("second", target2.GetValue(Class1.FooProperty)); + } + + [TestMethod] + public void this_Operator_Binds_Two_Way() + { + Class1 target1 = new Class1(); + Class1 target2 = new Class1(); + Binding binding = Class1.FooProperty.Bind().WithMode(BindingMode.TwoWay); + + target1.SetValue(Class1.FooProperty, "first"); + target2[binding] = target1[!Class1.FooProperty]; + Assert.AreEqual("first", target2.GetValue(Class1.FooProperty)); + target1.SetValue(Class1.FooProperty, "second"); + Assert.AreEqual("second", target2.GetValue(Class1.FooProperty)); + target2.SetValue(Class1.FooProperty, "third"); + Assert.AreEqual("third", target1.GetValue(Class1.FooProperty)); + } + + [TestMethod] + public void this_Operator_Binds_One_Time() + { + Class1 target1 = new Class1(); + Class1 target2 = new Class1(); + Binding binding = Class1.FooProperty.Bind().WithMode(BindingMode.OneTime); + + target1.SetValue(Class1.FooProperty, "first"); + target2[binding] = target1[!Class1.FooProperty]; + target1.SetValue(Class1.FooProperty, "second"); + + Assert.AreEqual("first", target2.GetValue(Class1.FooProperty)); + } + /// /// Returns an observable that returns a single value but does not complete. /// diff --git a/Perspex.UnitTests/PerspexPropertyTests.cs b/Perspex.UnitTests/PerspexPropertyTests.cs index 76e4f5873c..c874751757 100644 --- a/Perspex.UnitTests/PerspexPropertyTests.cs +++ b/Perspex.UnitTests/PerspexPropertyTests.cs @@ -19,7 +19,8 @@ namespace Perspex.UnitTests "test", typeof(Class1), "Foo", - false); + false, + BindingMode.OneWay); Assert.AreEqual("test", target.Name); Assert.AreEqual(typeof(string), target.PropertyType); @@ -34,7 +35,8 @@ namespace Perspex.UnitTests "test", typeof(Class1), "Foo", - false); + false, + BindingMode.OneWay); Assert.AreEqual("Foo", target.GetDefaultValue()); } @@ -46,7 +48,8 @@ namespace Perspex.UnitTests "test", typeof(Class1), "Foo", - false); + false, + BindingMode.OneWay); Assert.AreEqual("Foo", target.GetDefaultValue()); } @@ -58,7 +61,8 @@ namespace Perspex.UnitTests "test", typeof(Class3), "Foo", - false); + false, + BindingMode.OneWay); Assert.AreEqual("Foo", target.GetDefaultValue()); } @@ -70,7 +74,8 @@ namespace Perspex.UnitTests "test", typeof(Class1), "Foo", - false); + false, + BindingMode.OneWay); target.OverrideDefaultValue(typeof(Class2), "Bar"); diff --git a/Perspex/Binding.cs b/Perspex/Binding.cs new file mode 100644 index 0000000000..be39daf060 --- /dev/null +++ b/Perspex/Binding.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Perspex +{ + public enum BindingMode + { + Default, + OneWay, + TwoWay, + OneTime, + OneWayToSource, + } + + public struct Binding + { + public BindingMode Mode + { + get; + set; + } + + public BindingPriority Priority + { + get; + set; + } + + public PerspexProperty Property + { + get; + set; + } + + public PerspexObject Source + { + get; + set; + } + + public static Binding operator !(Binding binding) + { + return binding.WithMode(BindingMode.TwoWay); + } + + public static Binding operator ~(Binding binding) + { + return binding.WithMode(BindingMode.TwoWay); + } + + public Binding WithMode(BindingMode mode) + { + this.Mode = mode; + return this; + } + + public Binding WithPriority(BindingPriority priority) + { + this.Priority = priority; + return this; + } + } +} diff --git a/Perspex/Perspex.csproj b/Perspex/Perspex.csproj index 8b7d8e7802..a35b5aa8e6 100644 --- a/Perspex/Perspex.csproj +++ b/Perspex/Perspex.csproj @@ -69,6 +69,7 @@ + diff --git a/Perspex/PerspexObject.cs b/Perspex/PerspexObject.cs index 774ba70320..0ea7127f70 100644 --- a/Perspex/PerspexObject.cs +++ b/Perspex/PerspexObject.cs @@ -140,13 +140,45 @@ namespace Perspex } /// - /// Gets or sets the binding for a . + /// Gets or sets a binding for a . /// - /// The property. - public IObservable this[PerspexProperty.BindingAccessor property] + /// The binding information. + public Binding this[Binding binding] { - get { return this.GetObservable(property.Property); } - set { this.Bind(property.Property, value, property.Priority); } + get + { + return new Binding + { + Mode = binding.Mode, + Priority = binding.Priority, + Property = binding.Property, + Source = this, + }; + } + + set + { + BindingMode mode = (binding.Mode == BindingMode.Default) ? + binding.Property.DefaultBindingMode : + binding.Mode; + + switch (mode) + { + case BindingMode.Default: + case BindingMode.OneWay: + this.Bind(binding.Property, value.Source.GetObservable(value.Property), binding.Priority); + break; + case BindingMode.OneTime: + this.SetValue(binding.Property, value.Source.GetValue(value.Property)); + break; + case BindingMode.OneWayToSource: + value.Source.Bind(value.Property, this.GetObservable(binding.Property), binding.Priority); + break; + case BindingMode.TwoWay: + this.BindTwoWay(binding.Property, value.Source, value.Property); + break; + } + } } /// @@ -218,8 +250,8 @@ namespace Perspex /// /// Gets an observable for a . /// - /// - /// + /// The property. + /// An observable. public IObservable GetObservable(PerspexProperty property) { Contract.Requires(property != null); @@ -505,6 +537,28 @@ namespace Perspex return this.Bind((PerspexProperty)property, (IObservable)source, priority); } + /// + /// Initialites a two-way bind between s. + /// + /// The property on this object. + /// The source object. + /// The property on the source object. + /// + /// 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. + /// + public void BindTwoWay( + PerspexProperty property, + PerspexObject source, + PerspexProperty sourceProperty) + { + source.GetObservable(sourceProperty).Subscribe(x => this.SetValue(property, x)); + this.GetObservable(property).Subscribe(x => source.SetValue(sourceProperty, x)); + } + private PriorityValue CreatePriorityValue(PerspexProperty property) { PriorityValue result = new PriorityValue(property.Name, property.PropertyType); @@ -535,6 +589,11 @@ namespace Perspex return result; } + /// + /// Gets the default value for a property. + /// + /// The property. + /// The default value. private object GetDefaultValue(PerspexProperty property) { if (property.Inherits && this.inheritanceParent != null) diff --git a/Perspex/PerspexProperty.cs b/Perspex/PerspexProperty.cs index f3db1f5a9d..e16ef28d7f 100644 --- a/Perspex/PerspexProperty.cs +++ b/Perspex/PerspexProperty.cs @@ -45,12 +45,14 @@ namespace Perspex /// The type of the class that registers the property. /// The default value of the property. /// Whether the property inherits its value. + /// The default binding mode for the property. public PerspexProperty( string name, Type valueType, Type ownerType, object defaultValue, - bool inherits) + bool inherits, + BindingMode defaultBindingMode) { Contract.Requires(name != null); Contract.Requires(valueType != null); @@ -60,6 +62,7 @@ namespace Perspex this.PropertyType = valueType; this.OwnerType = ownerType; this.Inherits = inherits; + this.DefaultBindingMode = defaultBindingMode; this.defaultValues.Add(ownerType, defaultValue); } @@ -83,6 +86,12 @@ namespace Perspex /// public bool Inherits { get; private set; } + /// + /// Gets the default binding mode for the property. + /// + /// + public BindingMode DefaultBindingMode { get; private set; } + /// /// Gets an observable that is fired when this property changes on any /// instance. @@ -100,11 +109,13 @@ namespace Perspex /// The name of the property. /// The default value of the property. /// Whether the property inherits its value. - /// + /// The default binding mode for the property. + /// A public static PerspexProperty Register( string name, TValue defaultValue = default(TValue), - bool inherits = false) + bool inherits = false, + BindingMode defaultBindingMode = BindingMode.OneWay) where TOwner : PerspexObject { Contract.Requires(name != null); @@ -113,7 +124,8 @@ namespace Perspex name, typeof(TOwner), defaultValue, - inherits); + inherits, + defaultBindingMode); PerspexObject.Register(typeof(TOwner), result); @@ -129,11 +141,13 @@ namespace Perspex /// The name of the property. /// The default value of the property. /// Whether the property inherits its value. - /// + /// The default binding mode for the property. + /// A public static PerspexProperty RegisterAttached( string name, TValue defaultValue = default(TValue), - bool inherits = false) + bool inherits = false, + BindingMode defaultBindingMode = BindingMode.OneWay) where TOwner : PerspexObject { Contract.Requires(name != null); @@ -142,7 +156,8 @@ namespace Perspex name, typeof(TOwner), defaultValue, - inherits); + inherits, + defaultBindingMode); PerspexObject.Register(typeof(THost), result); @@ -154,10 +169,14 @@ namespace Perspex /// indexer. /// /// The property. - /// A describing the binding. - public static BindingAccessor operator!(PerspexProperty property) + /// A describing the binding. + public static Binding operator!(PerspexProperty property) { - return new BindingAccessor(property, BindingPriority.LocalValue); + return new Binding + { + Priority = BindingPriority.LocalValue, + Property = property, + }; } /// @@ -165,10 +184,30 @@ namespace Perspex /// indexer. /// /// The property. - /// A describing the binding. - public static BindingAccessor operator ~(PerspexProperty property) + /// A describing the binding. + public static Binding operator ~(PerspexProperty property) { - return new BindingAccessor(property, BindingPriority.TemplatedParent); + return new Binding + { + Priority = BindingPriority.TemplatedParent, + Property = property, + }; + } + + /// + /// Returns a binding accessor that can be passed to 's [] + /// operator to initiate a binding. + /// + /// A . + /// + /// The ! and ~ operators are short forms of this. + /// + public Binding Bind() + { + return new Binding + { + Property = this, + }; } /// @@ -239,27 +278,6 @@ namespace Perspex this.changed.OnNext(e); } - public class BindingAccessor - { - public BindingAccessor(PerspexProperty property, BindingPriority priority) - { - this.Property = property; - this.Priority = priority; - } - - public PerspexProperty Property - { - get; - private set; - } - - public BindingPriority Priority - { - get; - private set; - } - } - private class Unset { public override string ToString() @@ -281,12 +299,14 @@ namespace Perspex /// The type of the class that registers the property. /// The default value of the property. /// Whether the property inherits its value. + /// The default binding mode for the property. public PerspexProperty( string name, Type ownerType, TValue defaultValue, - bool inherits) - : base(name, typeof(TValue), ownerType, defaultValue, inherits) + bool inherits, + BindingMode defaultBindingMode) + : base(name, typeof(TValue), ownerType, defaultValue, inherits, defaultBindingMode) { Contract.Requires(name != null); Contract.Requires(ownerType != null); diff --git a/Perspex/PriorityValue.cs b/Perspex/PriorityValue.cs index 2ec892ca51..ffc67cccd6 100644 --- a/Perspex/PriorityValue.cs +++ b/Perspex/PriorityValue.cs @@ -239,7 +239,7 @@ namespace Perspex } /// - /// Called when an binding's value changes. + /// Called when a binding's value changes. /// /// The changed entry. private void EntryChanged(BindingEntry changed) @@ -251,7 +251,7 @@ namespace Perspex } /// - /// Called when an binding completes. + /// Called when a binding completes. /// /// The completed entry. private void EntryCompleted(BindingEntry entry) diff --git a/Perspex/Themes/Default/TreeViewItemStyle.cs b/Perspex/Themes/Default/TreeViewItemStyle.cs index 56253129e0..31c5cab537 100644 --- a/Perspex/Themes/Default/TreeViewItemStyle.cs +++ b/Perspex/Themes/Default/TreeViewItemStyle.cs @@ -49,6 +49,7 @@ namespace Perspex.Themes.Default new ToggleButton { Classes = new Classes("expander"), + [~~ToggleButton.IsCheckedProperty] = control[~TreeViewItem.IsExpandedProperty], }, new ContentPresenter {