From 133dd7aa80542c92054a848e572fa7cb33aae81e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Sep 2015 22:29:48 +0200 Subject: [PATCH] Enable AddOwner for direct PerspexProperties. --- src/Perspex.Base/PerspexObject.cs | 111 +++++++++++++----- src/Perspex.Base/PerspexProperty.cs | 84 ++++++++++++- src/Perspex.Base/PerspexProperty`1.cs | 69 ++++++++++- .../PerspexObjectTests_Binding.cs | 4 +- .../PerspexObjectTests_Direct.cs | 104 +++++++++++++++- .../PerspexObjectTests_GetValue.cs | 13 ++ .../PerspexObjectTests_SetValue.cs | 4 +- .../PerspexPropertyTests.cs | 40 ++++++- 8 files changed, 387 insertions(+), 42 deletions(-) diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index ab612008b2..2f722e853e 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/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)GetRegistered(property)).Getter(this); } else { @@ -462,24 +467,7 @@ namespace Perspex /// True if the property is registered, otherwise false. public bool IsRegistered(PerspexProperty property) { - Type type = GetType(); - - while (type != null) - { - List list; - - if (s_registered.TryGetValue(type, out list)) - { - if (list.Contains(property)) - { - return true; - } - } - - type = type.GetTypeInfo().BaseType; - } - - return false; + return FindRegistered(property) != null; } /// @@ -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)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)GetRegistered(property); + if (property.Setter == null) { throw new ArgumentException($"The property {property.Name} is readonly."); @@ -852,6 +842,59 @@ namespace Perspex } } + /// + /// Given a returns a registered perspex property that is + /// equal. + /// + /// The property. + /// The registered property or null if not found. + /// + /// 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 . + /// + public PerspexProperty FindRegistered(PerspexProperty property) + { + Type type = GetType(); + + while (type != null) + { + List 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; + } + + /// + /// Given a returns a registered perspex property that is + /// equal or throws if not found. + /// + /// The property. + /// The registered property. + public PerspexProperty GetRegistered(PerspexProperty property) + { + var result = FindRegistered(property); + + if (result == null) + { + ThrowNotRegistered(property); + } + + return result; + } + /// /// Called when a property is changed on the current . /// @@ -879,5 +922,15 @@ namespace Perspex { return string.Format("{0}.{1}", GetType().Name, property.Name); } + + /// + /// Throws an exception indicating that the specified property is not registered on this + /// object. + /// + /// The property + private void ThrowNotRegistered(PerspexProperty p) + { + throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}"); + } } } diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index 6fb2f77929..e0f2f980b4 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -15,13 +15,18 @@ namespace Perspex /// /// This class is analogous to DependencyProperty in WPF. /// - public class PerspexProperty + public class PerspexProperty : IEquatable { /// /// Represents an unset property value. /// public static readonly object UnsetValue = new Unset(); + /// + /// Gets the next ID that will be allocated to a property. + /// + private static int s_nextId = 1; + /// /// The default values for the property, by type. /// @@ -43,6 +48,11 @@ namespace Perspex private readonly Dictionary> _validation = new Dictionary>(); + /// + /// Gets the ID of the property. + /// + private int _id; + /// /// Initializes a new instance of the class. /// @@ -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++; + } + + /// + /// Initializes a new instance of the class. + /// + /// The direct property to copy. + /// A new getter. + /// A new setter. + protected PerspexProperty( + PerspexProperty source, + Func getter, + Action setter) + { + Contract.Requires(source != null); + Contract.Requires(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; } /// @@ -239,6 +280,28 @@ namespace Perspex /// internal Action Setter { get; } + /// + /// Tests two s for equality. + /// + /// The first property. + /// The second property. + /// True if the properties are equal, otherwise false. + public static bool operator ==(PerspexProperty a, PerspexProperty b) + { + return a?.Equals(b) ?? false; + } + + /// + /// Tests two s for unequality. + /// + /// The first property. + /// The second property. + /// True if the properties are equal, otherwise false. + public static bool operator !=(PerspexProperty a, PerspexProperty b) + { + return !a?.Equals(b) ?? false; + } + /// /// Registers a . /// @@ -373,6 +436,25 @@ namespace Perspex return result; } + /// + public override bool Equals(object obj) + { + var p = obj as PerspexProperty; + return p != null ? Equals(p) : false; + } + + /// + public bool Equals(PerspexProperty other) + { + return other != null && _id == other._id; + } + + /// + public override int GetHashCode() + { + return _id; + } + /// /// Returns a binding accessor that can be passed to 's [] /// operator to initiate a binding. diff --git a/src/Perspex.Base/PerspexProperty`1.cs b/src/Perspex.Base/PerspexProperty`1.cs index 00ee3b1cb7..a62d6eb599 100644 --- a/src/Perspex.Base/PerspexProperty`1.cs +++ b/src/Perspex.Base/PerspexProperty`1.cs @@ -53,7 +53,23 @@ namespace Perspex Type ownerType, Func getter, Action setter) - : base(name, typeof(TValue), ownerType, Cast(getter), Cast(setter)) + : base(name, typeof(TValue), ownerType, CastParamReturn(getter), CastParams(setter)) + { + Getter = getter; + Setter = setter; + } + + /// + /// Initializes a new instance of the class. + /// + /// The direct property to copy. + /// A new getter. + /// A new setter. + private PerspexProperty( + PerspexProperty source, + Func getter, + Action setter) + : base(source, CastParamReturn(getter), CastParams(setter)) { Getter = getter; Setter = setter; @@ -74,12 +90,33 @@ namespace Perspex /// /// The type of the additional owner. /// The property. - public PerspexProperty AddOwner() + public PerspexProperty AddOwner() 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; } + /// + /// Registers the direct property on another type. + /// + /// The type of the additional owner. + /// The property. + public PerspexProperty AddOwner( + Func getter, + Action setter) + where TOwner : PerspexObject + { + var result = new PerspexProperty(this, CastReturn(getter), CastParam1(setter)); + PerspexObject.Register(typeof(TOwner), result); + return result; + } + /// /// Gets the default value for the property on the specified type. /// @@ -109,24 +146,48 @@ namespace Perspex /// The owner type. /// The typed function. /// The untyped function. - private static Func Cast(Func f) + private static Func CastParamReturn(Func f) where TOwner : PerspexObject { return (f != null) ? o => f((TOwner)o) : (Func)null; } + /// + /// Casts a typed getter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Func CastReturn(Func f) + where TOwner : PerspexObject + { + return (f != null) ? o => f((TOwner)o) : (Func)null; + } + /// /// Casts a typed setter function to an untyped. /// /// The owner type. /// The typed function. /// The untyped function. - private static Action Cast(Action f) + private static Action CastParams(Action f) where TOwner : PerspexObject { return (f != null) ? (o, v) => f((TOwner)o, (TValue)v) : (Action)null; } + /// + /// Casts a typed setter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Action CastParam1(Action f) + where TOwner : PerspexObject + { + return (f != null) ? (o, v) => f((TOwner)o, v) : (Action)null; + } + /// /// Casts a typed validation function to an untyped. /// diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs index 20002d362a..7c8e60543d 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs @@ -39,7 +39,7 @@ namespace Perspex.Base.UnitTests { Class1 target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => { target.Bind(Class2.BarProperty, Observable.Return("foo")); }); @@ -212,7 +212,7 @@ namespace Perspex.Base.UnitTests { Class1 target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => { target[Class1.FooProperty] = Observable.Return("newvalue"); }); diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index 98d1051f4f..edc4b3e5d0 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/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 values = new List(); @@ -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(); @@ -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(); @@ -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(); + + 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(); + + 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 FooProperty = @@ -176,5 +260,19 @@ namespace Perspex.Base.UnitTests get { return _bar; } } } + + private class Class2 : PerspexObject + { + public static readonly PerspexProperty FooProperty = + Class1.FooProperty.AddOwner(o => o.Foo, (o, v) => o.Foo = v); + + private string _foo = "initial2"; + + public string Foo + { + get { return _foo; } + set { SetAndRaise(FooProperty, ref _foo, value); } + } + } } } diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs index 7fe24fc056..f143f56c56 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs +++ b/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(() => target.GetValue(Class1.FooProperty)); + } + private class Class1 : PerspexObject { public static readonly PerspexProperty FooProperty = @@ -66,5 +75,9 @@ namespace Perspex.Base.UnitTests set { InheritanceParent = value; } } } + + private class Class3 : PerspexObject + { + } } } diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs index ed79e12b44..7c7dce01aa 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs @@ -87,7 +87,7 @@ namespace Perspex.Base.UnitTests { Class1 target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => { target.SetValue(Class2.BarProperty, "invalid"); }); @@ -98,7 +98,7 @@ namespace Perspex.Base.UnitTests { Class1 target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => { target.SetValue(Class1.FooProperty, 123); }); diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs index b5326a5216..e1de37a7ca 100644 --- a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs +++ b/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("p1", typeof(Class1)); + var p2 = p1.AddOwner(); + + 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("d1", typeof(Class1), o => null, (o,v) => { }); + var p2 = p1.AddOwner(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("p1", typeof(Class1)); + + Assert.Throws(() => p1.AddOwner(o => null, (o, v) => { })); + } + + [Fact] + public void AddOwner_On_Direct_Property_Without_Getter_Or_Setter_Should_Throw() + { + var p1 = new PerspexProperty("e1", typeof(Class1), o => null, (o, v) => { }); + + Assert.Throws(() => p1.AddOwner()); + } + private class Class1 : PerspexObject { public static readonly PerspexProperty FooProperty = @@ -147,7 +185,7 @@ namespace Perspex.Base.UnitTests { } - private class Class3 + private class Class3 : PerspexObject { } }