Browse Source

Implemented direct PersexProperties.

pull/223/head
Steven Kirk 11 years ago
parent
commit
247831a6c8
  1. 277
      src/Perspex.Base/PerspexObject.cs
  2. 106
      src/Perspex.Base/PerspexProperty.cs
  3. 60
      src/Perspex.Base/PerspexProperty`1.cs
  4. 4
      src/Perspex.Base/PriorityValue.cs
  5. 1
      tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj
  6. 180
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs
  7. 12
      tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs
  8. 14
      tests/Perspex.Base.UnitTests/PriorityValueTests.cs

277
src/Perspex.Base/PerspexObject.cs

@ -112,11 +112,15 @@ namespace Perspex
foreach (var property in GetRegisteredProperties())
{
object value = property.IsDirect ?
property.Getter(this) :
property.GetDefaultValue(GetType());
var e = new PerspexPropertyChangedEventArgs(
this,
property,
PerspexProperty.UnsetValue,
property.GetDefaultValue(GetType()),
value,
BindingPriority.Unset);
property.NotifyInitialized(e);
@ -427,25 +431,27 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(property != null);
object result;
PriorityValue value;
if (_values.TryGetValue(property, out value))
if (property.IsDirect)
{
result = value.Value;
return property.Getter(this);
}
else
{
result = PerspexProperty.UnsetValue;
}
object result = PerspexProperty.UnsetValue;
PriorityValue value;
if (result == PerspexProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
if (_values.TryGetValue(property, out value))
{
result = value.Value;
}
return result;
if (result == PerspexProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
}
/// <summary>
@ -458,7 +464,14 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(property != null);
return (T)GetValue((PerspexProperty)property);
if (property.IsDirect)
{
return property.Getter(this);
}
else
{
return (T)GetValue((PerspexProperty)property);
}
}
/// <summary>
@ -524,35 +537,49 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(property != null);
PriorityValue v;
var originalValue = value;
if (!IsRegistered(property))
if (property.IsDirect)
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
GetType()));
}
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
property.Name,
originalValue,
originalValue?.GetType().FullName ?? "(null)"));
property.Setter(this, value);
}
if (!_values.TryGetValue(property, out v))
else
{
if (value == PerspexProperty.UnsetValue)
PriorityValue v;
var originalValue = value;
if (!IsRegistered(property))
{
return;
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
GetType()));
}
v = CreatePriorityValue(property);
_values.Add(property, v);
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
property.Name,
originalValue,
originalValue?.GetType().FullName ?? "(null)"));
}
if (!_values.TryGetValue(property, out v))
{
if (value == PerspexProperty.UnsetValue)
{
return;
}
v = CreatePriorityValue(property);
_values.Add(property, v);
}
v.SetValue(value, (int)priority);
}
_propertyLog.Verbose(
@ -560,7 +587,6 @@ namespace Perspex
property,
value,
priority);
v.SetDirectValue(value, (int)priority);
}
/// <summary>
@ -577,7 +603,19 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(property != null);
SetValue((PerspexProperty)property, value, priority);
if (property.IsDirect)
{
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
property.Setter(this, value);
}
else
{
SetValue((PerspexProperty)property, value, priority);
}
}
/// <summary>
@ -596,30 +634,47 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(property != null);
PriorityValue v;
IDescription description = source as IDescription;
if (!IsRegistered(property))
if (property.IsDirect)
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
GetType()));
}
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
if (!_values.TryGetValue(property, out v))
{
v = CreatePriorityValue(property);
_values.Add(property, v);
_propertyLog.Verbose(
"Bound {Property} to {Binding} with priority LocalValue",
property,
source);
return source.Subscribe(x => SetValue(property, x));
}
else
{
PriorityValue v;
IDescription description = source as IDescription;
_propertyLog.Verbose(
"Bound {Property} to {Binding} with priority {Priority}",
property,
source,
priority);
if (!IsRegistered(property))
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
GetType()));
}
return v.Add(source, (int)priority);
if (!_values.TryGetValue(property, out v))
{
v = CreatePriorityValue(property);
_values.Add(property, v);
}
_propertyLog.Verbose(
"Bound {Property} to {Binding} with priority {Priority}",
property,
source,
priority);
return v.Add(source, (int)priority);
}
}
/// <summary>
@ -639,7 +694,19 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(property != null);
return Bind((PerspexProperty)property, source.Select(x => (object)x), priority);
if (property.IsDirect)
{
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
return source.Subscribe(x => SetValue(property, x));
}
else
{
return Bind((PerspexProperty)property, source.Select(x => (object)x), priority);
}
}
/// <summary>
@ -713,6 +780,61 @@ namespace Perspex
{
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged(
PerspexProperty property,
object oldValue,
object newValue,
BindingPriority priority)
{
Contract.Requires<NullReferenceException>(property != null);
PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs(
this,
property,
oldValue,
newValue,
priority);
OnPropertyChanged(e);
property.NotifyChanged(e);
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
if (_inpcChanged != null)
{
PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, e2);
}
}
/// <summary>
/// Sets the backing field for a direct perspex property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
protected void SetAndRaise<T>(PerspexProperty<T> property, ref T field, T value)
{
if (!object.Equals(field, value))
{
var old = field;
field = value;
RaisePropertyChanged(property, old, value, BindingPriority.LocalValue);
}
}
/// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>.
/// </summary>
@ -799,42 +921,5 @@ namespace Perspex
{
return string.Format("{0}.{1}", GetType().Name, property.Name);
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
private void RaisePropertyChanged(
PerspexProperty property,
object oldValue,
object newValue,
BindingPriority priority)
{
Contract.Requires<NullReferenceException>(property != null);
PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs(
this,
property,
oldValue,
newValue,
priority);
OnPropertyChanged(e);
property.NotifyChanged(e);
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
if (_inpcChanged != null)
{
PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, e2);
}
}
}
}

106
src/Perspex.Base/PerspexProperty.cs

@ -87,6 +87,39 @@ namespace Perspex
}
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="valueType">The type of the property's value.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
public PerspexProperty(
string name,
Type valueType,
Type ownerType,
Func<PerspexObject, object> getter,
Action<PerspexObject, object> setter)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(valueType != null);
Contract.Requires<NullReferenceException>(ownerType != null);
Contract.Requires<NullReferenceException>(getter != null);
if (name.Contains("."))
{
throw new ArgumentException("'name' may not contain periods.");
}
Name = name;
PropertyType = valueType;
OwnerType = ownerType;
Getter = getter;
Setter = setter;
IsDirect = true;
}
/// <summary>
/// Gets the name of the property.
/// </summary>
@ -135,6 +168,11 @@ namespace Perspex
/// </value>
public bool IsAttached { get; }
/// <summary>
/// Gets a value indicating whether this is a direct property.
/// </summary>
public bool IsDirect { get; }
/// <summary>
/// Gets an observable that is fired when this property is initialized on a
/// new <see cref="PerspexObject"/> instance.
@ -191,6 +229,16 @@ namespace Perspex
};
}
/// <summary>
/// Gets the getter function for direct properties.
/// </summary>
internal Func<PerspexObject, object> Getter { get; }
/// <summary>
/// Gets the etter function for direct properties.
/// </summary>
internal Action<PerspexObject, object> Setter { get; }
/// <summary>
/// Registers a <see cref="PerspexProperty"/>.
/// </summary>
@ -226,6 +274,34 @@ namespace Perspex
return result;
}
/// <summary>
/// Registers a direct <see cref="PerspexProperty"/>.
/// </summary>
/// <typeparam name="TOwner">The type of the class that is registering the property.</typeparam>
/// <typeparam name="TValue">The type of the property's value.</typeparam>
/// <param name="name">The name of the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
/// <returns>A <see cref="PerspexProperty{TValue}"/></returns>
public static PerspexProperty<TValue> RegisterDirect<TOwner, TValue>(
string name,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter = null)
where TOwner : PerspexObject
{
Contract.Requires<NullReferenceException>(name != null);
PerspexProperty<TValue> result = new PerspexProperty<TValue>(
name,
typeof(TOwner),
Cast(getter),
Cast(setter));
PerspexObject.Register(typeof(TOwner), result);
return result;
}
/// <summary>
/// Registers an attached <see cref="PerspexProperty"/>.
/// </summary>
@ -453,9 +529,37 @@ namespace Perspex
_changed.OnNext(e);
}
/// <summary>
/// Casts a getter function accepting a typed owner to one accepting a
/// <see cref="PerspexObject"/>.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <typeparam name="TValue">The property value type.</typeparam>
/// <param name="f">The typed function.</param>
/// <returns>The untyped function.</returns>
private static Func<PerspexObject, TValue> Cast<TOwner, TValue>(Func<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? o => f((TOwner)o) : (Func<PerspexObject, TValue >)null;
}
/// <summary>
/// Casts a setter action accepting a typed owner to one accepting a
/// <see cref="PerspexObject"/>.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <typeparam name="TValue">The property value type.</typeparam>
/// <param name="f">The typed action.</param>
/// <returns>The untyped action.</returns>
private static Action<PerspexObject, TValue> Cast<TOwner, TValue>(Action<TOwner, TValue> f)
where TOwner : PerspexObject
{
return f != null ? (o, v) => f((TOwner)o, v) : (Action<PerspexObject, TValue>)null;
}
/// <summary>
/// Casts a validation function accepting a typed owner to one accepting a
/// <see cref="Perspex"/>.
/// <see cref="PerspexObject"/>.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <typeparam name="TValue">The property value type.</typeparam>

60
src/Perspex.Base/PerspexProperty`1.cs

@ -36,13 +36,39 @@ namespace Perspex
defaultValue,
inherits,
defaultBindingMode,
Convert(validate),
Cast(validate),
isAttached)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(ownerType != null);
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty{TValue}"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
public PerspexProperty(
string name,
Type ownerType,
Func<PerspexObject, TValue> getter,
Action<PerspexObject, TValue> setter)
: base(name, typeof(TValue), ownerType, Cast(getter), Cast(setter))
{
Getter = getter;
Setter = setter;
}
/// <summary>
/// Gets the getter function for direct properties.
/// </summary>
internal new Func<PerspexObject, TValue> Getter { get; }
/// <summary>
/// Gets the etter function for direct properties.
/// </summary>
internal new Action<PerspexObject, TValue> Setter { get; }
/// <summary>
/// Registers the property on another type.
/// </summary>
@ -78,11 +104,35 @@ namespace Perspex
}
/// <summary>
/// Converts from a typed validation function to an untyped.
/// Casts a typed getter function to an untyped.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <param name="f">The typed function.</param>
/// <returns>The untyped function.</returns>
private static Func<PerspexObject, object> Cast<TOwner>(Func<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? o => f((TOwner)o) : (Func<PerspexObject, object>)null;
}
/// <summary>
/// Casts a typed setter function to an untyped.
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <param name="f">The typed function.</param>
/// <returns>The untyped function.</returns>
private static Action<PerspexObject, object> Cast<TOwner>(Action<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? (o, v) => f((TOwner)o, (TValue)v) : (Action<PerspexObject, object>)null;
}
/// <summary>
/// Casts a typed validation function to an untyped.
/// </summary>
/// <param name="f">The typed validation function.</param>
/// <returns>The untyped validation function.</returns>
private static Func<PerspexObject, object, object> Convert(Func<PerspexObject, TValue, TValue> f)
private static Func<PerspexObject, object, object> Cast(Func<PerspexObject, TValue, TValue> f)
{
return f != null ? (o, v) => f(o, (TValue)v) : (Func<PerspexObject, object, object>)null;
}

4
src/Perspex.Base/PriorityValue.cs

@ -105,11 +105,11 @@ namespace Perspex
}
/// <summary>
/// Sets the direct value for a specified priority.
/// Sets the value for a specified priority.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="priority">The priority</param>
public void SetDirectValue(object value, int priority)
public void SetValue(object value, int priority)
{
GetLevel(priority).DirectValue = value;
}

1
tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj

@ -73,6 +73,7 @@
<Compile Include="Collections\PerspexDictionaryTests.cs" />
<Compile Include="Collections\PerspexListTests.cs" />
<Compile Include="Collections\PropertyChangedTracker.cs" />
<Compile Include="PerspexObjectTests_Direct.cs" />
<Compile Include="PerspexObjectTests_GetObservable.cs" />
<Compile Include="PerspexObjectTests_Validation.cs" />
<Compile Include="PerspexObjectTests_Binding.cs" />

180
tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs

@ -0,0 +1,180 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using Xunit;
namespace Perspex.Base.UnitTests
{
public class PerspexObjectTests_Direct
{
[Fact]
public void GetValue_Gets_Value()
{
var target = new Class1();
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
}
[Fact]
public void GetValue_Gets_Value_NonGeneric()
{
var target = new Class1();
Assert.Equal("initial", target.GetValue((PerspexProperty)Class1.FooProperty));
}
[Fact]
public void SetValue_Sets_Value()
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "newvalue");
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void SetValue_Sets_Value_NonGeneric()
{
var target = new Class1();
target.SetValue((PerspexProperty)Class1.FooProperty, "newvalue");
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void SetValue_Raises_PropertyChanged()
{
var target = new Class1();
bool raised = false;
target.PropertyChanged += (s, e) =>
raised = e.Property == Class1.FooProperty &&
(string)e.OldValue == "initial" &&
(string)e.NewValue == "newvalue" &&
e.Priority == BindingPriority.LocalValue;
target.SetValue(Class1.FooProperty, "newvalue");
Assert.True(raised);
}
[Fact]
public void Direct_Property_Works_As_Binding_Source()
{
var target = new Class1();
List<string> values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
target.Foo = "newvalue";
Assert.Equal(new[] { "initial", "newvalue" }, values);
}
[Fact]
public void Direct_Property_Can_Be_Bound()
{
var target = new Class1();
var source = new Subject<string>();
var sub = target.Bind(Class1.FooProperty, source);
Assert.Equal("initial", target.Foo);
source.OnNext("first");
Assert.Equal("first", target.Foo);
source.OnNext("second");
Assert.Equal("second", target.Foo);
sub.Dispose();
source.OnNext("third");
Assert.Equal("second", target.Foo);
}
[Fact]
public void Direct_Property_Can_Be_Bound_NonGeneric()
{
var target = new Class1();
var source = new Subject<string>();
var sub = target.Bind((PerspexProperty)Class1.FooProperty, source);
Assert.Equal("initial", target.Foo);
source.OnNext("first");
Assert.Equal("first", target.Foo);
source.OnNext("second");
Assert.Equal("second", target.Foo);
sub.Dispose();
source.OnNext("third");
Assert.Equal("second", target.Foo);
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Set()
{
var target = new Class1();
Assert.Throws<ArgumentException>(() =>
target.SetValue(Class1.BarProperty, "newvalue"));
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Set_NonGeneric()
{
var target = new Class1();
Assert.Throws<ArgumentException>(() =>
target.SetValue((PerspexProperty)Class1.BarProperty, "newvalue"));
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Bound()
{
var target = new Class1();
var source = new Subject<string>();
Assert.Throws<ArgumentException>(() =>
target.Bind(Class1.BarProperty, source));
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Bound_NonGeneric()
{
var target = new Class1();
var source = new Subject<string>();
Assert.Throws<ArgumentException>(() =>
target.Bind(Class1.BarProperty, source));
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
PerspexProperty.RegisterDirect<Class1, string>("Foo", o => o.Foo, (o, v) => o.Foo = v);
public static readonly PerspexProperty<string> BarProperty =
PerspexProperty.RegisterDirect<Class1, string>("Bar", o => o.Bar);
private string _foo = "initial";
private string _bar = "bar";
public string Foo
{
get { return _foo; }
set { SetAndRaise(FooProperty, ref _foo, value); }
}
public string Bar
{
get { return _bar; }
}
}
}
}

12
tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs

@ -125,6 +125,18 @@ namespace Perspex.Base.UnitTests
Assert.Equal("newvalue", value);
}
[Fact]
public void IsDirect_Property_Set_On_Direct_PerspexProperty()
{
PerspexProperty<string> target = new PerspexProperty<string>(
"test",
typeof(Class1),
o => null,
(o, v) => { });
Assert.True(target.IsDirect);
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =

14
tests/Perspex.Base.UnitTests/PriorityValueTests.cs

@ -47,7 +47,7 @@ namespace Perspex.Base.UnitTests
var target = new PriorityValue("Test", typeof(string));
target.Add(Single("foo"), 0);
target.SetDirectValue("bar", 0);
target.SetValue("bar", 0);
Assert.Equal("bar", target.Value);
}
@ -60,7 +60,7 @@ namespace Perspex.Base.UnitTests
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
target.SetValue("first", 0);
Assert.Equal("first", target.Value);
source.OnNext("second");
Assert.Equal("second", target.Value);
@ -76,7 +76,7 @@ namespace Perspex.Base.UnitTests
target.Add(nonActive, 0);
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
target.SetValue("first", 0);
Assert.Equal("first", target.Value);
nonActive.OnNext("second");
Assert.Equal("second", target.Value);
@ -92,7 +92,7 @@ namespace Perspex.Base.UnitTests
target.Add(nonActive, 1);
target.Add(source, 1);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 1);
target.SetValue("first", 1);
Assert.Equal("first", target.Value);
nonActive.OnNext("second");
Assert.Equal("first", target.Value);
@ -106,7 +106,7 @@ namespace Perspex.Base.UnitTests
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetDirectValue("first", 0);
target.SetValue("first", 0);
Assert.Equal("first", target.Value);
source.OnNext("second");
Assert.Equal("second", target.Value);
@ -267,9 +267,9 @@ namespace Perspex.Base.UnitTests
{
var target = new PriorityValue("Test", typeof(int), x => Math.Min((int)x, 10));
target.SetDirectValue(5, 0);
target.SetValue(5, 0);
Assert.Equal(5, target.Value);
target.SetDirectValue(15, 0);
target.SetValue(15, 0);
Assert.Equal(10, target.Value);
}

Loading…
Cancel
Save