Browse Source

Enable AddOwner for direct PerspexProperties.

Steven Kirk 10 years ago
parent
commit
133dd7aa80

+ 82 - 29
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()}");
+        }
     }
 }

+ 83 - 1
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.

+ 65 - 4
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>

+ 2 - 2
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");
             });

+ 101 - 3
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 - 0
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
+        {
+        }
     }
 }

+ 2 - 2
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);
             });

+ 39 - 1
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
         {
         }
     }