diff --git a/Perspex.Base.UnitTests/PerspexObjectTests.cs b/Perspex.Base.UnitTests/PerspexObjectTests.cs index 2da8d172d4..1e26768e37 100644 --- a/Perspex.Base.UnitTests/PerspexObjectTests.cs +++ b/Perspex.Base.UnitTests/PerspexObjectTests.cs @@ -4,6 +4,7 @@ // // ----------------------------------------------------------------------- +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Perspex.Base.UnitTests { using System; @@ -29,7 +30,7 @@ namespace Perspex.Base.UnitTests { string[] names = PerspexObject.GetProperties(typeof(Class1)).Select(x => x.Name).ToArray(); - CollectionAssert.AreEqual(new[] { "Foo", "Baz" }, names); + CollectionAssert.AreEqual(new[] { "Foo", "Baz", "Qux" }, names); } [TestMethod] @@ -37,7 +38,7 @@ namespace Perspex.Base.UnitTests { string[] names = PerspexObject.GetProperties(typeof(Class2)).Select(x => x.Name).ToArray(); - CollectionAssert.AreEqual(new[] { "Bar", "Foo", "Baz" }, names); + CollectionAssert.AreEqual(new[] { "Bar", "Foo", "Baz", "Qux" }, names); } [TestMethod] @@ -169,6 +170,30 @@ namespace Perspex.Base.UnitTests target.SetValue(Class1.FooProperty, 123); } + [TestMethod] + public void SetValue_Causes_Coercion() + { + Class1 target = new Class1(); + + target.SetValue(Class1.QuxProperty, 5); + Assert.AreEqual(5, target.GetValue(Class1.QuxProperty)); + target.SetValue(Class1.QuxProperty, -5); + Assert.AreEqual(0, target.GetValue(Class1.QuxProperty)); + target.SetValue(Class1.QuxProperty, 15); + Assert.AreEqual(10, target.GetValue(Class1.QuxProperty)); + } + + [TestMethod] + public void CoerceValue_Causes_Recoercion() + { + Class1 target = new Class1(); + + target.SetValue(Class1.QuxProperty, 7); + Assert.AreEqual(7, target.GetValue(Class1.QuxProperty)); + target.MaxQux = 5; + target.CoerceValue(Class1.QuxProperty); + } + [TestMethod] public void GetObservable_Returns_Initial_Value() { @@ -503,6 +528,21 @@ namespace Perspex.Base.UnitTests public static readonly PerspexProperty BazProperty = PerspexProperty.Register("Baz", "bazdefault", true); + + public static readonly PerspexProperty QuxProperty = + PerspexProperty.Register("Qux", coerce: Coerce); + + public int MaxQux { get; set; } + + public Class1() + { + this.MaxQux = 10; + } + + private static int Coerce(PerspexObject instance, int value) + { + return Math.Min(Math.Max(value, 0), ((Class1)instance).MaxQux); + } } private class Class2 : Class1 diff --git a/Perspex.Base.UnitTests/PerspexPropertyTests.cs b/Perspex.Base.UnitTests/PerspexPropertyTests.cs index eb298c45d0..e469fb1271 100644 --- a/Perspex.Base.UnitTests/PerspexPropertyTests.cs +++ b/Perspex.Base.UnitTests/PerspexPropertyTests.cs @@ -20,7 +20,8 @@ namespace Perspex.Base.UnitTests typeof(Class1), "Foo", false, - BindingMode.OneWay); + BindingMode.OneWay, + null); Assert.AreEqual("test", target.Name); Assert.AreEqual(typeof(string), target.PropertyType); @@ -36,7 +37,8 @@ namespace Perspex.Base.UnitTests typeof(Class1), "Foo", false, - BindingMode.OneWay); + BindingMode.OneWay, + null); Assert.AreEqual("Foo", target.GetDefaultValue()); } @@ -49,7 +51,8 @@ namespace Perspex.Base.UnitTests typeof(Class1), "Foo", false, - BindingMode.OneWay); + BindingMode.OneWay, + null); Assert.AreEqual("Foo", target.GetDefaultValue()); } @@ -62,7 +65,8 @@ namespace Perspex.Base.UnitTests typeof(Class3), "Foo", false, - BindingMode.OneWay); + BindingMode.OneWay, + null); Assert.AreEqual("Foo", target.GetDefaultValue()); } @@ -75,7 +79,8 @@ namespace Perspex.Base.UnitTests typeof(Class1), "Foo", false, - BindingMode.OneWay); + BindingMode.OneWay, + null); target.OverrideDefaultValue(typeof(Class2), "Bar"); diff --git a/Perspex.Base/PerspexObject.cs b/Perspex.Base/PerspexObject.cs index b0980cf687..cb5aef1fbf 100644 --- a/Perspex.Base/PerspexObject.cs +++ b/Perspex.Base/PerspexObject.cs @@ -634,9 +634,51 @@ namespace Perspex this.GetObservable(property).Subscribe(x => source.SetValue(sourceProperty, x)); } + /// + /// Forces the specified property to be re-coerced. + /// + /// The property. + public void CoerceValue(PerspexProperty property) + { + PriorityValue value; + + if (this.values.TryGetValue(property, out value)) + { + value.Coerce(); + } + } + + /// + /// Forces re-coercion of properties when a property value changes. + /// + /// The property to that affects coercion. + /// The affected properties. + protected static void AffectsCoercion(PerspexProperty property, params PerspexProperty[] affected) + { + property.Changed.Subscribe(e => + { + foreach (var p in affected) + { + e.Sender.CoerceValue(p); + } + }); + } + + /// + /// Creates a for a . + /// + /// The property. + /// The . private PriorityValue CreatePriorityValue(PerspexProperty property) { - PriorityValue result = new PriorityValue(property.Name, property.PropertyType); + Func coerce = null; + + if (property.Coerce != null) + { + coerce = v => property.Coerce(this, v); + } + + PriorityValue result = new PriorityValue(property.Name, property.PropertyType, coerce); result.Changed.Subscribe(x => { diff --git a/Perspex.Base/PerspexProperty.cs b/Perspex.Base/PerspexProperty.cs index a5cd7bdb1f..2484e8db31 100644 --- a/Perspex.Base/PerspexProperty.cs +++ b/Perspex.Base/PerspexProperty.cs @@ -39,6 +39,11 @@ namespace Perspex /// private Subject changed = new Subject(); + /// + /// The coerce function. + /// + private Func coerce; + /// /// Initializes a new instance of the class. /// @@ -48,13 +53,15 @@ namespace Perspex /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A coercion function. public PerspexProperty( string name, Type valueType, Type ownerType, object defaultValue, bool inherits, - BindingMode defaultBindingMode) + BindingMode defaultBindingMode, + Func coerce) { Contract.Requires(name != null); Contract.Requires(valueType != null); @@ -63,9 +70,10 @@ namespace Perspex this.Name = name; this.PropertyType = valueType; this.OwnerType = ownerType; + this.defaultValues.Add(ownerType, defaultValue); this.Inherits = inherits; this.DefaultBindingMode = defaultBindingMode; - this.defaultValues.Add(ownerType, defaultValue); + this.Coerce = coerce; } /// @@ -94,6 +102,11 @@ namespace Perspex /// public BindingMode DefaultBindingMode { get; private set; } + /// + /// Gets the property's coerce function. + /// + public Func Coerce { get; private set; } + /// /// Gets an observable that is fired when this property is initialized on a /// new instance. @@ -127,12 +140,14 @@ namespace Perspex /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A coercion function. /// A public static PerspexProperty Register( string name, TValue defaultValue = default(TValue), bool inherits = false, - BindingMode defaultBindingMode = BindingMode.OneWay) + BindingMode defaultBindingMode = BindingMode.OneWay, + Func coerce = null) where TOwner : PerspexObject { Contract.Requires(name != null); @@ -142,7 +157,8 @@ namespace Perspex typeof(TOwner), defaultValue, inherits, - defaultBindingMode); + defaultBindingMode, + coerce); PerspexObject.Register(typeof(TOwner), result); @@ -159,12 +175,14 @@ namespace Perspex /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A coercion function. /// A public static PerspexProperty RegisterAttached( string name, TValue defaultValue = default(TValue), bool inherits = false, - BindingMode defaultBindingMode = BindingMode.OneWay) + BindingMode defaultBindingMode = BindingMode.OneWay, + Func coerce = null) where TOwner : PerspexObject { Contract.Requires(name != null); @@ -174,7 +192,8 @@ namespace Perspex typeof(TOwner), defaultValue, inherits, - defaultBindingMode); + defaultBindingMode, + coerce); PerspexObject.Register(typeof(THost), result); @@ -321,13 +340,22 @@ namespace Perspex /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A coercion function. public PerspexProperty( string name, Type ownerType, TValue defaultValue, bool inherits, - BindingMode defaultBindingMode) - : base(name, typeof(TValue), ownerType, defaultValue, inherits, defaultBindingMode) + BindingMode defaultBindingMode, + Func coerce) + : base( + name, + typeof(TValue), + ownerType, + defaultValue, + inherits, + defaultBindingMode, + Convert(coerce)) { Contract.Requires(name != null); Contract.Requires(ownerType != null); @@ -353,5 +381,15 @@ namespace Perspex { return (TValue)this.GetDefaultValue(typeof(T)); } + + /// + /// Converts from a typed coercion function to an untyped. + /// + /// The typed coercion function. + /// Te untyped coercion function. + private static Func Convert(Func f) + { + return f != null ? (o, v) => f(o, (TValue)v) : (Func)null; + } } } diff --git a/Perspex.Base/PriorityValue.cs b/Perspex.Base/PriorityValue.cs index 9c843430e4..ccfcb628de 100644 --- a/Perspex.Base/PriorityValue.cs +++ b/Perspex.Base/PriorityValue.cs @@ -51,17 +51,21 @@ namespace Perspex /// private object value; + private Func coerce; + /// /// Initializes a new instance of the class. /// /// The name of the property. /// The value type. - public PriorityValue(string name, Type valueType) + /// An optional coercion function. + public PriorityValue(string name, Type valueType, Func coerce = null) { this.name = name; this.valueType = valueType; this.value = PerspexProperty.UnsetValue; this.ValuePriority = int.MaxValue; + this.coerce = coerce; } /// @@ -238,6 +242,33 @@ namespace Perspex return this.bindings; } + /// + /// Causes a re-coercion of the value. + /// + public void Coerce() + { + if (this.coerce != null) + { + this.SetValue(this.Value, this.ValuePriority); + } + } + + /// + /// Throws an exception if is invalid. + /// + /// The value. + private void VerifyValidValue(object value) + { + if (!IsValidValue(value, this.valueType)) + { + throw new InvalidOperationException(string.Format( + "Invalid value for Property '{0}': {1} ({2})", + this.name, + value, + value.GetType().FullName)); + } + } + /// /// Called when a binding's value changes. /// @@ -266,13 +297,12 @@ namespace Perspex /// The priority of the binding which produced the value. private void SetValue(object value, int priority) { - if (!IsValidValue(value, this.valueType)) + VerifyValidValue(value); + + if (this.coerce != null) { - throw new InvalidOperationException(string.Format( - "Invalid value for Property '{0}': {1} ({2})", - this.name, - value, - value.GetType().FullName)); + value = this.coerce(value); + VerifyValidValue(value); } object old = this.value; diff --git a/Perspex.Controls/ScrollViewer.cs b/Perspex.Controls/ScrollViewer.cs index f37954659e..65dca3e99b 100644 --- a/Perspex.Controls/ScrollViewer.cs +++ b/Perspex.Controls/ScrollViewer.cs @@ -17,7 +17,7 @@ namespace Perspex.Controls PerspexProperty.Register("Extent"); public static readonly PerspexProperty OffsetProperty = - PerspexProperty.Register("Offset"); + PerspexProperty.Register("Offset", coerce: CoerceOffset); public static readonly PerspexProperty ViewportProperty = PerspexProperty.Register("Viewport"); @@ -28,6 +28,12 @@ namespace Perspex.Controls private ScrollBar verticalScrollBar; + static ScrollViewer() + { + AffectsCoercion(ExtentProperty, OffsetProperty); + AffectsCoercion(ViewportProperty, OffsetProperty); + } + public Size Extent { get { return this.GetValue(ExtentProperty); } @@ -103,5 +109,28 @@ namespace Perspex.Controls this.Bind(OffsetProperty, offset); } + + private static double Clamp(double value, double min, double max) + { + return (value < min) ? min : (value > max) ? max : value; + } + + private static Vector CoerceOffset(PerspexObject o, Vector value) + { + ScrollViewer scrollViewer = o as ScrollViewer; + + if (scrollViewer != null) + { + var extent = scrollViewer.Extent; + var viewport = scrollViewer.Viewport; + var maxX = Math.Max(extent.Width - viewport.Width, 0); + var maxY = Math.Max(extent.Height - viewport.Height, 0); + return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); + } + else + { + return value; + } + } } }