Browse Source

Added coercion to PerspexProperties.

And use it to make sure ScrollViewer's offset is in range.
pull/10/head
Steven Kirk 11 years ago
parent
commit
688f6532c8
  1. 44
      Perspex.Base.UnitTests/PerspexObjectTests.cs
  2. 15
      Perspex.Base.UnitTests/PerspexPropertyTests.cs
  3. 44
      Perspex.Base/PerspexObject.cs
  4. 54
      Perspex.Base/PerspexProperty.cs
  5. 44
      Perspex.Base/PriorityValue.cs
  6. 31
      Perspex.Controls/ScrollViewer.cs

44
Perspex.Base.UnitTests/PerspexObjectTests.cs

@ -4,6 +4,7 @@
// </copyright>
// -----------------------------------------------------------------------
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<string> BazProperty =
PerspexProperty.Register<Class1, string>("Baz", "bazdefault", true);
public static readonly PerspexProperty<int> QuxProperty =
PerspexProperty.Register<Class1, int>("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

15
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<Class1>());
}
@ -49,7 +51,8 @@ namespace Perspex.Base.UnitTests
typeof(Class1),
"Foo",
false,
BindingMode.OneWay);
BindingMode.OneWay,
null);
Assert.AreEqual("Foo", target.GetDefaultValue<Class2>());
}
@ -62,7 +65,8 @@ namespace Perspex.Base.UnitTests
typeof(Class3),
"Foo",
false,
BindingMode.OneWay);
BindingMode.OneWay,
null);
Assert.AreEqual("Foo", target.GetDefaultValue<Class2>());
}
@ -75,7 +79,8 @@ namespace Perspex.Base.UnitTests
typeof(Class1),
"Foo",
false,
BindingMode.OneWay);
BindingMode.OneWay,
null);
target.OverrideDefaultValue(typeof(Class2), "Bar");

44
Perspex.Base/PerspexObject.cs

@ -634,9 +634,51 @@ namespace Perspex
this.GetObservable(property).Subscribe(x => source.SetValue(sourceProperty, x));
}
/// <summary>
/// Forces the specified property to be re-coerced.
/// </summary>
/// <param name="property">The property.</param>
public void CoerceValue(PerspexProperty property)
{
PriorityValue value;
if (this.values.TryGetValue(property, out value))
{
value.Coerce();
}
}
/// <summary>
/// Forces re-coercion of properties when a property value changes.
/// </summary>
/// <param name="property">The property to that affects coercion.</param>
/// <param name="affected">The affected properties.</param>
protected static void AffectsCoercion(PerspexProperty property, params PerspexProperty[] affected)
{
property.Changed.Subscribe(e =>
{
foreach (var p in affected)
{
e.Sender.CoerceValue(p);
}
});
}
/// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The <see cref="PriorityValue"/>.</returns>
private PriorityValue CreatePriorityValue(PerspexProperty property)
{
PriorityValue result = new PriorityValue(property.Name, property.PropertyType);
Func<object, object> 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 =>
{

54
Perspex.Base/PerspexProperty.cs

@ -39,6 +39,11 @@ namespace Perspex
/// </summary>
private Subject<PerspexPropertyChangedEventArgs> changed = new Subject<PerspexPropertyChangedEventArgs>();
/// <summary>
/// The coerce function.
/// </summary>
private Func<PerspexObject, object, object> coerce;
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
@ -48,13 +53,15 @@ namespace Perspex
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param>
public PerspexProperty(
string name,
Type valueType,
Type ownerType,
object defaultValue,
bool inherits,
BindingMode defaultBindingMode)
BindingMode defaultBindingMode,
Func<PerspexObject, object, object> coerce)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(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;
}
/// <summary>
@ -94,6 +102,11 @@ namespace Perspex
/// <returns></returns>
public BindingMode DefaultBindingMode { get; private set; }
/// <summary>
/// Gets the property's coerce function.
/// </summary>
public Func<PerspexObject, object, object> Coerce { get; private set; }
/// <summary>
/// Gets an observable that is fired when this property is initialized on a
/// new <see cref="PerspexObject"/> instance.
@ -127,12 +140,14 @@ namespace Perspex
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param>
/// <returns>A <see cref="PerspexProperty{TValue}"/></returns>
public static PerspexProperty<TValue> Register<TOwner, TValue>(
string name,
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay)
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<PerspexObject, TValue, TValue> coerce = null)
where TOwner : PerspexObject
{
Contract.Requires<NullReferenceException>(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
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param>
/// <returns>A <see cref="PerspexProperty{TValue}"/></returns>
public static PerspexProperty<TValue> RegisterAttached<TOwner, THost, TValue>(
string name,
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay)
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<PerspexObject, TValue, TValue> coerce = null)
where TOwner : PerspexObject
{
Contract.Requires<NullReferenceException>(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
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param>
public PerspexProperty(
string name,
Type ownerType,
TValue defaultValue,
bool inherits,
BindingMode defaultBindingMode)
: base(name, typeof(TValue), ownerType, defaultValue, inherits, defaultBindingMode)
BindingMode defaultBindingMode,
Func<PerspexObject, TValue, TValue> coerce)
: base(
name,
typeof(TValue),
ownerType,
defaultValue,
inherits,
defaultBindingMode,
Convert(coerce))
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(ownerType != null);
@ -353,5 +381,15 @@ namespace Perspex
{
return (TValue)this.GetDefaultValue(typeof(T));
}
/// <summary>
/// Converts from a typed coercion function to an untyped.
/// </summary>
/// <param name="f">The typed coercion function.</param>
/// <returns>Te untyped coercion function.</returns>
private static Func<PerspexObject, object, object> Convert(Func<PerspexObject, TValue, TValue> f)
{
return f != null ? (o, v) => f(o, (TValue)v) : (Func<PerspexObject, object, object>)null;
}
}
}

44
Perspex.Base/PriorityValue.cs

@ -51,17 +51,21 @@ namespace Perspex
/// </summary>
private object value;
private Func<object, object> coerce;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="valueType">The value type.</param>
public PriorityValue(string name, Type valueType)
/// <param name="coerce">An optional coercion function.</param>
public PriorityValue(string name, Type valueType, Func<object, object> coerce = null)
{
this.name = name;
this.valueType = valueType;
this.value = PerspexProperty.UnsetValue;
this.ValuePriority = int.MaxValue;
this.coerce = coerce;
}
/// <summary>
@ -238,6 +242,33 @@ namespace Perspex
return this.bindings;
}
/// <summary>
/// Causes a re-coercion of the value.
/// </summary>
public void Coerce()
{
if (this.coerce != null)
{
this.SetValue(this.Value, this.ValuePriority);
}
}
/// <summary>
/// Throws an exception if <paramref name="value"/> is invalid.
/// </summary>
/// <param name="value">The value.</param>
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));
}
}
/// <summary>
/// Called when a binding's value changes.
/// </summary>
@ -266,13 +297,12 @@ namespace Perspex
/// <param name="priority">The priority of the binding which produced the value.</param>
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;

31
Perspex.Controls/ScrollViewer.cs

@ -17,7 +17,7 @@ namespace Perspex.Controls
PerspexProperty.Register<ScrollViewer, Size>("Extent");
public static readonly PerspexProperty<Vector> OffsetProperty =
PerspexProperty.Register<ScrollViewer, Vector>("Offset");
PerspexProperty.Register<ScrollViewer, Vector>("Offset", coerce: CoerceOffset);
public static readonly PerspexProperty<Size> ViewportProperty =
PerspexProperty.Register<ScrollViewer, Size>("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;
}
}
}
}

Loading…
Cancel
Save