Browse Source

Handle implicit conversions in perspex properties.

Fixes #49.
pull/58/head
Steven Kirk 11 years ago
parent
commit
26015acfd9
  1. 1
      Perspex.Base/Perspex.Base.csproj
  2. 6
      Perspex.Base/PerspexObject.cs
  3. 5
      Perspex.Base/PerspexProperty.cs
  4. 71
      Perspex.Base/PriorityValue.cs
  5. 83
      Perspex.Base/Utilities/TypeUtilities.cs
  6. 2
      Perspex.Themes.Default/ButtonStyle.cs
  7. 13
      Tests/Perspex.Base.UnitTests/GlobalSuppressions.cs
  8. 1
      Tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj
  9. 54
      Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs

1
Perspex.Base/Perspex.Base.csproj

@ -64,6 +64,7 @@
<Compile Include="Threading\DispatcherTimer.cs" />
<Compile Include="Threading\MainLoop.cs" />
<Compile Include="Threading\PerspexScheduler.cs" />
<Compile Include="Utilities\TypeUtilities.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">

6
Perspex.Base/PerspexObject.cs

@ -16,6 +16,8 @@ namespace Perspex
using Perspex.Reactive;
using Serilog;
using Serilog.Core.Enrichers;
using Perspex.Utilities;
/// <summary>
/// The priority of a binding.
@ -514,13 +516,13 @@ namespace Perspex
this.GetType()));
}
if (!PriorityValue.IsValidValue(value, property.PropertyType))
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
property.Name,
value,
value.GetType().FullName));
value?.GetType().FullName ?? "(null)"));
}
if (!this.values.TryGetValue(property, out v))

5
Perspex.Base/PerspexProperty.cs

@ -6,6 +6,7 @@
namespace Perspex
{
using Perspex.Utilities;
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
@ -346,7 +347,7 @@ namespace Perspex
/// <returns>True if the value is valid, otherwise false.</returns>
public bool IsValidValue(object value)
{
return PriorityValue.IsValidValue(value, this.PropertyType);
return TypeUtilities.TryCast(this.PropertyType, value, out value);
}
/// <summary>
@ -368,7 +369,7 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(type != null);
if (!this.IsValidValue(defaultValue))
if (!TypeUtilities.TryCast(this.PropertyType, defaultValue, out defaultValue))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",

71
Perspex.Base/PriorityValue.cs

@ -12,17 +12,18 @@ namespace Perspex
using System.Reactive.Subjects;
using System.Reflection;
using System.Text;
using Perspex.Utilities;
/// <summary>
/// Maintains a list of prioritised bindings together with a current value.
/// </summary>
/// <remarks>
/// Bindings, in the form of <see cref="IObservable<object>"/>s are added to the object using
/// Bindings, in the form of <see cref="IObservable{object}"/>s are added to the object using
/// the <see cref="Add"/> method. With the observable is passed a priority, where lower values
/// represent higher priorites. The current <see cref="Value"/> is selected from the highest
/// priority binding that doesn't return <see cref="PerspexProperty.UnsetValue"/>. Where there
/// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes, the <see cref="Changed"/> observable is
/// has a higher priority. Each time the value changes, the <see cref="Changed"/> observable is
/// fired with the old and new values.
/// </remarks>
internal class PriorityValue
@ -100,39 +101,6 @@ namespace Perspex
private set;
}
/// <summary>
/// Checks whether a value is valid for a type.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="propertyType">The type.</param>
/// <returns>True if the value is valid, otherwise false.</returns>
public static bool IsValidValue(object value, Type propertyType)
{
TypeInfo type = propertyType.GetTypeInfo();
if (value == PerspexProperty.UnsetValue)
{
return true;
}
else if (value == null)
{
if (type.IsValueType &&
(!type.IsGenericType || !(type.GetGenericTypeDefinition() == typeof(Nullable<>))))
{
return false;
}
}
else
{
if (!type.IsAssignableFrom(value.GetType().GetTypeInfo()))
{
return false;
}
}
return true;
}
/// <summary>
/// Adds a new binding.
/// </summary>
@ -172,7 +140,7 @@ namespace Perspex
}
/// <summary>
/// Returns diagnostic string that can help the user debug the bindings in effect on
/// Returns diagnostic string that can help the user debug the bindings in effect on
/// this object.
/// </summary>
/// <returns>A diagnostic string.</returns>
@ -188,7 +156,7 @@ namespace Perspex
b.AppendLine();
}
b.Append(this.ValuePriority == level.Key ? "*" : "");
b.Append(this.ValuePriority == level.Key ? "*" : string.Empty);
b.Append("Priority ");
b.Append(level.Key);
b.Append(": ");
@ -199,7 +167,7 @@ namespace Perspex
foreach (var binding in level.Value.Bindings)
{
b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : "");
b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty);
b.Append(binding.Description ?? binding.Observable.GetType().Name);
b.Append(": ");
b.AppendLine(binding.Value?.ToString() ?? "(null)");
@ -254,7 +222,14 @@ namespace Perspex
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
this.VerifyValidValue(value);
if (!TypeUtilities.TryCast(this.valueType, value, out value))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
this.name,
value,
value?.GetType().FullName ?? "(null)"));
}
var old = this.value;
@ -268,26 +243,10 @@ namespace Perspex
this.changed.OnNext(Tuple.Create(old, this.value));
}
/// <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 the value for a priority level changes.
/// </summary>
/// <param name="changed">The changed entry.</param>
/// <param name="level">The priority level of the changed entry.</param>
private void ValueChanged(PriorityLevel level)
{
if (level.Priority <= this.ValuePriority)

83
Perspex.Base/Utilities/TypeUtilities.cs

@ -0,0 +1,83 @@
// -----------------------------------------------------------------------
// <copyright file="TypeUtilities.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Utilities
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
/// <summary>
/// Provides utilities for working with types at runtime.
/// </summary>
internal static class TypeUtilities
{
private static readonly Dictionary<Type, List<Type>> Conversions = new Dictionary<Type, List<Type>>() {
{ typeof(decimal), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
{ typeof(double), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(float), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(ulong), new List<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
{ typeof(long), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
{ typeof(uint), new List<Type> { typeof(byte), typeof(ushort), typeof(char) } },
{ typeof(int), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
{ typeof(ushort), new List<Type> { typeof(byte), typeof(char) } },
{ typeof(short), new List<Type> { typeof(byte) } }
};
/// <summary>
/// Try to cast a value to a type, using implicit conversions if possible.
/// </summary>
/// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</param>
/// <param name="result">If sucessful, contains the cast value.</param>
/// <returns>True if the cast was sucessful, otherwise false.</returns>
public static bool TryCast(Type to, object value, out object result)
{
Contract.Requires<NullReferenceException>(to != null);
if (value == null)
{
var t = to.GetTypeInfo();
result = null;
return !t.IsValueType || (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(Nullable<>)));
}
var from = value.GetType();
if (value == PerspexProperty.UnsetValue)
{
result = value;
return true;
}
else if (to.GetTypeInfo().IsAssignableFrom(from.GetTypeInfo()))
{
result = value;
return true;
}
else if (Conversions.ContainsKey(to) && Conversions[to].Contains(from))
{
result = Convert.ChangeType(value, to);
return true;
}
else
{
var cast = from.GetTypeInfo()
.GetDeclaredMethods("op_Implicit")
.FirstOrDefault(m => m.ReturnType == to);
if (cast != null)
{
result = cast.Invoke(null, new[] { value });
return true;
}
}
result = null;
return false;
}
}
}

2
Perspex.Themes.Default/ButtonStyle.cs

@ -37,7 +37,7 @@ namespace Perspex.Themes.Default
{
new Setter(Button.BackgroundProperty, new SolidColorBrush(0xffdddddd)),
new Setter(Button.BorderBrushProperty, new SolidColorBrush(0xff707070)),
new Setter(Button.BorderThicknessProperty, 2.0),
new Setter(Button.BorderThicknessProperty, 2),
new Setter(Button.ForegroundProperty, new SolidColorBrush(0xff000000)),
},
},

13
Tests/Perspex.Base.UnitTests/GlobalSuppressions.cs

@ -0,0 +1,13 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1600:Elements must be documented",
Justification = "Tests should be self-documenting")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.NamingRules",
"SA1300:Element must begin with upper-case letter",
Justification = "Some tests must begin with lower-case letters")]

1
Tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj

@ -78,6 +78,7 @@
</Choose>
<ItemGroup>
<Compile Include="Collections\PerspexListTests.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="PerspexPropertyTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PerspexObjectTests.cs" />

54
Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs

@ -35,7 +35,7 @@ namespace Perspex.Base.UnitTests
{
string[] names = PerspexObject.GetProperties(typeof(Class2)).Select(x => x.Name).ToArray();
Assert.Equal(new[] { "Bar", "Flob", "Foo", "Baz", "Qux" }, names);
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux" }, names);
}
[Fact]
@ -176,7 +176,35 @@ namespace Perspex.Base.UnitTests
{
Class2 target = new Class2();
target.SetValue(Class2.FlobProperty, 4);
target.SetValue((PerspexProperty)Class2.FlobProperty, 4);
var value = target.GetValue(Class2.FlobProperty);
Assert.IsType<double>(value);
Assert.Equal(4, value);
}
[Fact]
public void SetValue_Respects_Implicit_Conversions()
{
Class2 target = new Class2();
target.SetValue((PerspexProperty)Class2.FlobProperty, new ImplictDouble(4));
var value = target.GetValue(Class2.FlobProperty);
Assert.IsType<double>(value);
Assert.Equal(4, value);
}
[Fact]
public void SetValue_Can_Convert_To_Nullable()
{
Class2 target = new Class2();
target.SetValue((PerspexProperty)Class2.FredProperty, 4.0);
var value = target.GetValue(Class2.FredProperty);
Assert.IsType<double>(value);
Assert.Equal(4, value);
}
[Fact]
@ -616,13 +644,13 @@ namespace Perspex.Base.UnitTests
public static readonly PerspexProperty<int> QuxProperty =
PerspexProperty.Register<Class1, int>("Qux", coerce: Coerce);
public int MaxQux { get; set; }
public Class1()
{
this.MaxQux = 10;
}
public int MaxQux { get; set; }
private static int Coerce(PerspexObject instance, int value)
{
return Math.Min(Math.Max(value, 0), ((Class1)instance).MaxQux);
@ -637,6 +665,9 @@ namespace Perspex.Base.UnitTests
public static readonly PerspexProperty<double> FlobProperty =
PerspexProperty.Register<Class2, double>("Flob");
public static readonly PerspexProperty<double?> FredProperty =
PerspexProperty.Register<Class2, double?>("Fred");
static Class2()
{
FooProperty.OverrideDefaultValue(typeof(Class2), "foooverride");
@ -648,5 +679,20 @@ namespace Perspex.Base.UnitTests
set { this.InheritanceParent = value; }
}
}
private class ImplictDouble
{
public ImplictDouble(double value)
{
this.Value = value;
}
public double Value { get; }
public static implicit operator double(ImplictDouble v)
{
return v.Value;
}
}
}
}

Loading…
Cancel
Save