Browse Source

Enable AddOwner for direct PerspexProperties.

pull/223/head
Steven Kirk 11 years ago
parent
commit
133dd7aa80
  1. 111
      src/Perspex.Base/PerspexObject.cs
  2. 84
      src/Perspex.Base/PerspexProperty.cs
  3. 69
      src/Perspex.Base/PerspexProperty`1.cs
  4. 4
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs
  5. 104
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs
  6. 13
      tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs
  7. 4
      tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs
  8. 40
      tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs

111
src/Perspex.Base/PerspexObject.cs

@ -391,13 +391,18 @@ namespace Perspex
if (property.IsDirect)
{
return property.Getter(this);
return GetRegistered(property).Getter(this);
}
else
{
object result = PerspexProperty.UnsetValue;
PriorityValue value;
if (!IsRegistered(property))
{
ThrowNotRegistered(property);
}
if (_values.TryGetValue(property, out value))
{
result = value.Value;
@ -424,7 +429,7 @@ namespace Perspex
if (property.IsDirect)
{
return property.Getter(this);
return ((PerspexProperty<T>)GetRegistered(property)).Getter(this);
}
else
{
@ -462,24 +467,7 @@ namespace Perspex
/// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(PerspexProperty property)
{
Type type = GetType();
while (type != null)
{
List<PerspexProperty> list;
if (s_registered.TryGetValue(type, out list))
{
if (list.Contains(property))
{
return true;
}
}
type = type.GetTypeInfo().BaseType;
}
return false;
return FindRegistered(property) != null;
}
/// <summary>
@ -497,6 +485,8 @@ namespace Perspex
if (property.IsDirect)
{
property = GetRegistered(property);
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
@ -511,15 +501,12 @@ namespace Perspex
if (!IsRegistered(property))
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
GetType()));
ThrowNotRegistered(property);
}
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
throw new InvalidOperationException(string.Format(
throw new ArgumentException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
property.Name,
originalValue,
@ -563,6 +550,8 @@ namespace Perspex
if (property.IsDirect)
{
property = (PerspexProperty<T>)GetRegistered(property);
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
@ -594,6 +583,8 @@ namespace Perspex
if (property.IsDirect)
{
property = GetRegistered(property);
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
@ -613,10 +604,7 @@ namespace Perspex
if (!IsRegistered(property))
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
GetType()));
ThrowNotRegistered(property);
}
if (!_values.TryGetValue(property, out v))
@ -654,6 +642,8 @@ namespace Perspex
if (property.IsDirect)
{
property = (PerspexProperty<T>)GetRegistered(property);
if (property.Setter == null)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
@ -852,6 +842,59 @@ namespace Perspex
}
}
/// <summary>
/// Given a <see cref="PerspexProperty"/> returns a registered perspex property that is
/// equal.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The registered property or null if not found.</returns>
/// <remarks>
/// Calling AddOwner on a direct PerspexProperty creates new new PerspexProperty with
/// an overridden getter and setter. This property is a different object but is equal
/// according to <see cref="object.Equals(object)"/>.
/// </remarks>
public PerspexProperty FindRegistered(PerspexProperty property)
{
Type type = GetType();
while (type != null)
{
List<PerspexProperty> list;
if (s_registered.TryGetValue(type, out list))
{
var index = list.IndexOf(property);
if (index != -1)
{
return list[index];
}
}
type = type.GetTypeInfo().BaseType;
}
return null;
}
/// <summary>
/// Given a <see cref="PerspexProperty"/> returns a registered perspex property that is
/// equal or throws if not found.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The registered property.</returns>
public PerspexProperty GetRegistered(PerspexProperty property)
{
var result = FindRegistered(property);
if (result == null)
{
ThrowNotRegistered(property);
}
return result;
}
/// <summary>
/// Called when a property is changed on the current <see cref="InheritanceParent"/>.
/// </summary>
@ -879,5 +922,15 @@ namespace Perspex
{
return string.Format("{0}.{1}", GetType().Name, property.Name);
}
/// <summary>
/// Throws an exception indicating that the specified property is not registered on this
/// object.
/// </summary>
/// <param name="p">The property</param>
private void ThrowNotRegistered(PerspexProperty p)
{
throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}");
}
}
}

84
src/Perspex.Base/PerspexProperty.cs

@ -15,13 +15,18 @@ namespace Perspex
/// <remarks>
/// This class is analogous to DependencyProperty in WPF.
/// </remarks>
public class PerspexProperty
public class PerspexProperty : IEquatable<PerspexProperty>
{
/// <summary>
/// Represents an unset property value.
/// </summary>
public static readonly object UnsetValue = new Unset();
/// <summary>
/// Gets the next ID that will be allocated to a property.
/// </summary>
private static int s_nextId = 1;
/// <summary>
/// The default values for the property, by type.
/// </summary>
@ -43,6 +48,11 @@ namespace Perspex
private readonly Dictionary<Type, Func<PerspexObject, object, object>> _validation =
new Dictionary<Type, Func<PerspexObject, object, object>>();
/// <summary>
/// Gets the ID of the property.
/// </summary>
private int _id;
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
@ -80,6 +90,7 @@ namespace Perspex
Inherits = inherits;
DefaultBindingMode = defaultBindingMode;
IsAttached = isAttached;
_id = s_nextId++;
if (validate != null)
{
@ -118,6 +129,36 @@ namespace Perspex
Getter = getter;
Setter = setter;
IsDirect = true;
_id = s_nextId++;
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="source">The direct property to copy.</param>
/// <param name="getter">A new getter.</param>
/// <param name="setter">A new setter.</param>
protected PerspexProperty(
PerspexProperty source,
Func<PerspexObject, object> getter,
Action<PerspexObject, object> setter)
{
Contract.Requires<NullReferenceException>(source != null);
Contract.Requires<NullReferenceException>(getter != null);
if (!source.IsDirect)
{
throw new InvalidOperationException(
"This method can only be called on direct PerspexProperties.");
}
Name = source.Name;
PropertyType = source.PropertyType;
OwnerType = source.OwnerType;
Getter = getter;
Setter = setter;
IsDirect = true;
_id = source._id;
}
/// <summary>
@ -239,6 +280,28 @@ namespace Perspex
/// </summary>
internal Action<PerspexObject, object> Setter { get; }
/// <summary>
/// Tests two <see cref="PerspexProperty"/>s for equality.
/// </summary>
/// <param name="a">The first property.</param>
/// <param name="b">The second property.</param>
/// <returns>True if the properties are equal, otherwise false.</returns>
public static bool operator ==(PerspexProperty a, PerspexProperty b)
{
return a?.Equals(b) ?? false;
}
/// <summary>
/// Tests two <see cref="PerspexProperty"/>s for unequality.
/// </summary>
/// <param name="a">The first property.</param>
/// <param name="b">The second property.</param>
/// <returns>True if the properties are equal, otherwise false.</returns>
public static bool operator !=(PerspexProperty a, PerspexProperty b)
{
return !a?.Equals(b) ?? false;
}
/// <summary>
/// Registers a <see cref="PerspexProperty"/>.
/// </summary>
@ -373,6 +436,25 @@ namespace Perspex
return result;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
var p = obj as PerspexProperty;
return p != null ? Equals(p) : false;
}
/// <inheritdoc/>
public bool Equals(PerspexProperty other)
{
return other != null && _id == other._id;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return _id;
}
/// <summary>
/// Returns a binding accessor that can be passed to <see cref="PerspexObject"/>'s []
/// operator to initiate a binding.

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

@ -53,7 +53,23 @@ namespace Perspex
Type ownerType,
Func<PerspexObject, TValue> getter,
Action<PerspexObject, TValue> setter)
: base(name, typeof(TValue), ownerType, Cast(getter), Cast(setter))
: base(name, typeof(TValue), ownerType, CastParamReturn(getter), CastParams(setter))
{
Getter = getter;
Setter = setter;
}
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="source">The direct property to copy.</param>
/// <param name="getter">A new getter.</param>
/// <param name="setter">A new setter.</param>
private PerspexProperty(
PerspexProperty source,
Func<PerspexObject, TValue> getter,
Action<PerspexObject, TValue> setter)
: base(source, CastParamReturn(getter), CastParams(setter))
{
Getter = getter;
Setter = setter;
@ -74,12 +90,33 @@ namespace Perspex
/// </summary>
/// <typeparam name="TOwner">The type of the additional owner.</typeparam>
/// <returns>The property.</returns>
public PerspexProperty<TValue> AddOwner<TOwner>()
public PerspexProperty<TValue> AddOwner<TOwner>() where TOwner : PerspexObject
{
if (IsDirect)
{
throw new InvalidOperationException(
"You must provide a new getter and setter when calling AddOwner on a direct PerspexProperty.");
}
PerspexObject.Register(typeof(TOwner), this);
return this;
}
/// <summary>
/// Registers the direct property on another type.
/// </summary>
/// <typeparam name="TOwner">The type of the additional owner.</typeparam>
/// <returns>The property.</returns>
public PerspexProperty<TValue> AddOwner<TOwner>(
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter)
where TOwner : PerspexObject
{
var result = new PerspexProperty<TValue>(this, CastReturn(getter), CastParam1(setter));
PerspexObject.Register(typeof(TOwner), result);
return result;
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// </summary>
@ -109,24 +146,48 @@ namespace Perspex
/// <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)
private static Func<PerspexObject, object> CastParamReturn<TOwner>(Func<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? o => f((TOwner)o) : (Func<PerspexObject, object>)null;
}
/// <summary>
/// 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, TValue> CastReturn<TOwner>(Func<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? o => f((TOwner)o) : (Func<PerspexObject, TValue>)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)
private static Action<PerspexObject, object> CastParams<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 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, TValue> CastParam1<TOwner>(Action<TOwner, TValue> f)
where TOwner : PerspexObject
{
return (f != null) ? (o, v) => f((TOwner)o, v) : (Action<PerspexObject, TValue>)null;
}
/// <summary>
/// Casts a typed validation function to an untyped.
/// </summary>

4
tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs

@ -39,7 +39,7 @@ namespace Perspex.Base.UnitTests
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
Assert.Throws<ArgumentException>(() =>
{
target.Bind(Class2.BarProperty, Observable.Return("foo"));
});
@ -212,7 +212,7 @@ namespace Perspex.Base.UnitTests
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
Assert.Throws<ArgumentException>(() =>
{
target[Class1.FooProperty] = Observable.Return("newvalue");
});

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

@ -64,7 +64,7 @@ namespace Perspex.Base.UnitTests
}
[Fact]
public void Direct_Property_Works_As_Binding_Source()
public void GetObservable_Returns_Values()
{
var target = new Class1();
List<string> values = new List<string>();
@ -76,7 +76,7 @@ namespace Perspex.Base.UnitTests
}
[Fact]
public void Direct_Property_Can_Be_Bound()
public void Bind_Binds_Property_Value()
{
var target = new Class1();
var source = new Subject<string>();
@ -96,7 +96,7 @@ namespace Perspex.Base.UnitTests
}
[Fact]
public void Direct_Property_Can_Be_Bound_NonGeneric()
public void Bind_Binds_Property_Value_NonGeneric()
{
var target = new Class1();
var source = new Subject<string>();
@ -153,6 +153,90 @@ namespace Perspex.Base.UnitTests
target.Bind(Class1.BarProperty, source));
}
[Fact]
public void GetValue_Gets_Value_On_AddOwnered_Property()
{
var target = new Class2();
Assert.Equal("initial2", target.GetValue(Class2.FooProperty));
}
[Fact]
public void GetValue_Gets_Value_On_AddOwnered_Property_Using_Original()
{
var target = new Class2();
Assert.Equal("initial2", target.GetValue(Class1.FooProperty));
}
[Fact]
public void GetValue_Gets_Value_On_AddOwnered_Property_Using_Original_NonGeneric()
{
var target = new Class2();
Assert.Equal("initial2", target.GetValue((PerspexProperty)Class1.FooProperty));
}
[Fact]
public void SetValue_Sets_Value_On_AddOwnered_Property_Using_Original()
{
var target = new Class2();
target.SetValue(Class1.FooProperty, "newvalue");
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void SetValue_Sets_Value_On_AddOwnered_Property_Using_Original_NonGeneric()
{
var target = new Class2();
target.SetValue((PerspexProperty)Class1.FooProperty, "newvalue");
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void Bind_Binds_AddOwnered_Property_Value()
{
var target = new Class2();
var source = new Subject<string>();
var sub = target.Bind(Class1.FooProperty, source);
Assert.Equal("initial2", 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 Bind_Binds_AddOwnered_Property_Value_NonGeneric()
{
var target = new Class2();
var source = new Subject<string>();
var sub = target.Bind((PerspexProperty)Class1.FooProperty, source);
Assert.Equal("initial2", 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);
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
@ -176,5 +260,19 @@ namespace Perspex.Base.UnitTests
get { return _bar; }
}
}
private class Class2 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
Class1.FooProperty.AddOwner<Class2>(o => o.Foo, (o, v) => o.Foo = v);
private string _foo = "initial2";
public string Foo
{
get { return _foo; }
set { SetAndRaise(FooProperty, ref _foo, value); }
}
}
}
}

13
tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs

@ -1,6 +1,7 @@
// 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 Xunit;
namespace Perspex.Base.UnitTests
@ -44,6 +45,14 @@ namespace Perspex.Base.UnitTests
Assert.Equal("changed", child.GetValue(Class1.BazProperty));
}
[Fact]
public void GetValue_Throws_Exception_For_Unregistered_Property()
{
var target = new Class3();
Assert.Throws<ArgumentException>(() => target.GetValue(Class1.FooProperty));
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
@ -66,5 +75,9 @@ namespace Perspex.Base.UnitTests
set { InheritanceParent = value; }
}
}
private class Class3 : PerspexObject
{
}
}
}

4
tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs

@ -87,7 +87,7 @@ namespace Perspex.Base.UnitTests
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
Assert.Throws<ArgumentException>(() =>
{
target.SetValue(Class2.BarProperty, "invalid");
});
@ -98,7 +98,7 @@ namespace Perspex.Base.UnitTests
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
Assert.Throws<ArgumentException>(() =>
{
target.SetValue(Class1.FooProperty, 123);
});

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

@ -137,6 +137,44 @@ namespace Perspex.Base.UnitTests
Assert.True(target.IsDirect);
}
[Fact]
public void AddOwnered_Property_Should_Equal_Original()
{
var p1 = new PerspexProperty<string>("p1", typeof(Class1));
var p2 = p1.AddOwner<Class3>();
Assert.Equal(p1, p2);
Assert.Equal(p1.GetHashCode(), p2.GetHashCode());
Assert.True(p1 == p2);
}
[Fact]
public void AddOwnered_Direct_Property_Should_Equal_Original()
{
var p1 = new PerspexProperty<string>("d1", typeof(Class1), o => null, (o,v) => { });
var p2 = p1.AddOwner<Class3>(o => null, (o, v) => { });
Assert.Equal(p1, p2);
Assert.Equal(p1.GetHashCode(), p2.GetHashCode());
Assert.True(p1 == p2);
}
[Fact]
public void AddOwner_With_Getter_And_Setter_On_Standard_Property_Should_Throw()
{
var p1 = new PerspexProperty<string>("p1", typeof(Class1));
Assert.Throws<InvalidOperationException>(() => p1.AddOwner<Class3>(o => null, (o, v) => { }));
}
[Fact]
public void AddOwner_On_Direct_Property_Without_Getter_Or_Setter_Should_Throw()
{
var p1 = new PerspexProperty<string>("e1", typeof(Class1), o => null, (o, v) => { });
Assert.Throws<InvalidOperationException>(() => p1.AddOwner<Class3>());
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
@ -147,7 +185,7 @@ namespace Perspex.Base.UnitTests
{
}
private class Class3
private class Class3 : PerspexObject
{
}
}

Loading…
Cancel
Save