Browse Source

Implemented two-way binding.

And hooked up the TreeViewItem's expander button. The hit testing on it
is a bit off though...
pull/4/head
Steven Kirk 12 years ago
parent
commit
8e7be781f8
  1. 104
      Perspex.UnitTests/PerspexObjectTests.cs
  2. 15
      Perspex.UnitTests/PerspexPropertyTests.cs
  3. 66
      Perspex/Binding.cs
  4. 1
      Perspex/Perspex.csproj
  5. 73
      Perspex/PerspexObject.cs
  6. 92
      Perspex/PerspexProperty.cs
  7. 4
      Perspex/PriorityValue.cs
  8. 1
      Perspex/Themes/Default/TreeViewItemStyle.cs

104
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));
}
/// <summary>
/// Returns an observable that returns a single value but does not complete.
/// </summary>

15
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<Class1>());
}
@ -46,7 +48,8 @@ namespace Perspex.UnitTests
"test",
typeof(Class1),
"Foo",
false);
false,
BindingMode.OneWay);
Assert.AreEqual("Foo", target.GetDefaultValue<Class2>());
}
@ -58,7 +61,8 @@ namespace Perspex.UnitTests
"test",
typeof(Class3),
"Foo",
false);
false,
BindingMode.OneWay);
Assert.AreEqual("Foo", target.GetDefaultValue<Class2>());
}
@ -70,7 +74,8 @@ namespace Perspex.UnitTests
"test",
typeof(Class1),
"Foo",
false);
false,
BindingMode.OneWay);
target.OverrideDefaultValue(typeof(Class2), "Bar");

66
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;
}
}
}

1
Perspex/Perspex.csproj

@ -69,6 +69,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Application.cs" />
<Compile Include="Binding.cs" />
<Compile Include="BindingExtensions.cs" />
<Compile Include="Controls\TreeDataTemplate.cs" />
<Compile Include="Controls\HeaderedContentControl.cs" />

73
Perspex/PerspexObject.cs

@ -140,13 +140,45 @@ namespace Perspex
}
/// <summary>
/// Gets or sets the binding for a <see cref="PerspexProperty"/>.
/// Gets or sets a binding for a <see cref="PerspexProperty"/>.
/// </summary>
/// <param name="property">The property.</param>
public IObservable<object> this[PerspexProperty.BindingAccessor property]
/// <param name="binding">The binding information.</param>
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;
}
}
}
/// <summary>
@ -218,8 +250,8 @@ namespace Perspex
/// <summary>
/// Gets an observable for a <see cref="PerspexProperty"/>.
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
/// <param name="property">The property.</param>
/// <returns>An observable.</returns>
public IObservable<object> GetObservable(PerspexProperty property)
{
Contract.Requires<NullReferenceException>(property != null);
@ -505,6 +537,28 @@ namespace Perspex
return this.Bind((PerspexProperty)property, (IObservable<object>)source, priority);
}
/// <summary>
/// Initialites a two-way bind between <see cref="PerspexProperty"/>s.
/// </summary>
/// <param name="property">The property on this object.</param>
/// <param name="source">The source object.</param>
/// <param name="sourceProperty">The property on the source object.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
/// <remarks>
/// The binding is first carried out from <paramref name="source"/> to this. Two-way
/// bindings are always at the LocalValue priority.
/// </remarks>
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;
}
/// <summary>
/// Gets the default value for a property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The default value.</returns>
private object GetDefaultValue(PerspexProperty property)
{
if (property.Inherits && this.inheritanceParent != null)

92
Perspex/PerspexProperty.cs

@ -45,12 +45,14 @@ namespace Perspex
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
public PerspexProperty(
string name,
Type valueType,
Type ownerType,
object defaultValue,
bool inherits)
bool inherits,
BindingMode defaultBindingMode)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(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
/// </summary>
public bool Inherits { get; private set; }
/// <summary>
/// Gets the default binding mode for the property.
/// </summary>
/// <returns></returns>
public BindingMode DefaultBindingMode { get; private set; }
/// <summary>
/// Gets an observable that is fired when this property changes on any
/// <see cref="PerspexObject"/> instance.
@ -100,11 +109,13 @@ namespace Perspex
/// <param name="name">The name of the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <returns></returns>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <returns>A <see cref="PerspexProperty{TValue}"/></returns>
public static PerspexProperty<TValue> Register<TOwner, TValue>(
string name,
TValue defaultValue = default(TValue),
bool inherits = false)
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay)
where TOwner : PerspexObject
{
Contract.Requires<NullReferenceException>(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
/// <param name="name">The name of the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <returns></returns>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <returns>A <see cref="PerspexProperty{TValue}"/></returns>
public static PerspexProperty<TValue> RegisterAttached<TOwner, THost, TValue>(
string name,
TValue defaultValue = default(TValue),
bool inherits = false)
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay)
where TOwner : PerspexObject
{
Contract.Requires<NullReferenceException>(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.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>A <see cref="BindingAccessor"/> describing the binding.</returns>
public static BindingAccessor operator!(PerspexProperty property)
/// <returns>A <see cref="Binding"/> describing the binding.</returns>
public static Binding operator!(PerspexProperty property)
{
return new BindingAccessor(property, BindingPriority.LocalValue);
return new Binding
{
Priority = BindingPriority.LocalValue,
Property = property,
};
}
/// <summary>
@ -165,10 +184,30 @@ namespace Perspex
/// indexer.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>A <see cref="BindingAccessor"/> describing the binding.</returns>
public static BindingAccessor operator ~(PerspexProperty property)
/// <returns>A <see cref="Binding"/> describing the binding.</returns>
public static Binding operator ~(PerspexProperty property)
{
return new BindingAccessor(property, BindingPriority.TemplatedParent);
return new Binding
{
Priority = BindingPriority.TemplatedParent,
Property = property,
};
}
/// <summary>
/// Returns a binding accessor that can be passed to <see cref="PerspexObject"/>'s []
/// operator to initiate a binding.
/// </summary>
/// <returns>A <see cref="Binding"/>.</returns>
/// <remarks>
/// The ! and ~ operators are short forms of this.
/// </remarks>
public Binding Bind()
{
return new Binding
{
Property = this,
};
}
/// <summary>
@ -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
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
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<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(ownerType != null);

4
Perspex/PriorityValue.cs

@ -239,7 +239,7 @@ namespace Perspex
}
/// <summary>
/// Called when an binding's value changes.
/// Called when a binding's value changes.
/// </summary>
/// <param name="changed">The changed entry.</param>
private void EntryChanged(BindingEntry changed)
@ -251,7 +251,7 @@ namespace Perspex
}
/// <summary>
/// Called when an binding completes.
/// Called when a binding completes.
/// </summary>
/// <param name="changed">The completed entry.</param>
private void EntryCompleted(BindingEntry entry)

1
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
{

Loading…
Cancel
Save