diff --git a/Perspex.Base/Perspex.Base.csproj b/Perspex.Base/Perspex.Base.csproj index 53f67bda25..7a79bd16f2 100644 --- a/Perspex.Base/Perspex.Base.csproj +++ b/Perspex.Base/Perspex.Base.csproj @@ -64,6 +64,7 @@ + diff --git a/Perspex.Base/PerspexObject.cs b/Perspex.Base/PerspexObject.cs index af82369b78..edc050e59b 100644 --- a/Perspex.Base/PerspexObject.cs +++ b/Perspex.Base/PerspexObject.cs @@ -16,6 +16,8 @@ namespace Perspex using Perspex.Reactive; using Serilog; using Serilog.Core.Enrichers; + using Perspex.Utilities; + /// /// 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)) diff --git a/Perspex.Base/PerspexProperty.cs b/Perspex.Base/PerspexProperty.cs index e6184ca510..d0f9d15037 100644 --- a/Perspex.Base/PerspexProperty.cs +++ b/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 /// True if the value is valid, otherwise false. public bool IsValidValue(object value) { - return PriorityValue.IsValidValue(value, this.PropertyType); + return TypeUtilities.TryCast(this.PropertyType, value, out value); } /// @@ -368,7 +369,7 @@ namespace Perspex { Contract.Requires(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})", diff --git a/Perspex.Base/PriorityValue.cs b/Perspex.Base/PriorityValue.cs index d269d3c2a0..41e6c884fc 100644 --- a/Perspex.Base/PriorityValue.cs +++ b/Perspex.Base/PriorityValue.cs @@ -12,17 +12,18 @@ namespace Perspex using System.Reactive.Subjects; using System.Reflection; using System.Text; + using Perspex.Utilities; /// /// Maintains a list of prioritised bindings together with a current value. /// /// - /// Bindings, in the form of s are added to the object using + /// Bindings, in the form of s are added to the object using /// the method. With the observable is passed a priority, where lower values /// represent higher priorites. The current is selected from the highest /// priority binding that doesn't return . 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 observable is + /// has a higher priority. Each time the value changes, the observable is /// fired with the old and new values. /// internal class PriorityValue @@ -100,39 +101,6 @@ namespace Perspex private set; } - /// - /// Checks whether a value is valid for a type. - /// - /// The value. - /// The type. - /// True if the value is valid, otherwise false. - 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; - } - /// /// Adds a new binding. /// @@ -172,7 +140,7 @@ namespace Perspex } /// - /// 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. /// /// A diagnostic string. @@ -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 /// The priority level that the value came from. 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)); } - /// - /// 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 the value for a priority level changes. /// - /// The changed entry. + /// The priority level of the changed entry. private void ValueChanged(PriorityLevel level) { if (level.Priority <= this.ValuePriority) diff --git a/Perspex.Base/Utilities/TypeUtilities.cs b/Perspex.Base/Utilities/TypeUtilities.cs new file mode 100644 index 0000000000..6ef08616ae --- /dev/null +++ b/Perspex.Base/Utilities/TypeUtilities.cs @@ -0,0 +1,83 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Utilities +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + /// + /// Provides utilities for working with types at runtime. + /// + internal static class TypeUtilities + { + private static readonly Dictionary> Conversions = new Dictionary>() { + { typeof(decimal), new List { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } }, + { typeof(double), new List { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } }, + { typeof(float), new List { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } }, + { typeof(ulong), new List { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } }, + { typeof(long), new List { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } }, + { typeof(uint), new List { typeof(byte), typeof(ushort), typeof(char) } }, + { typeof(int), new List { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } }, + { typeof(ushort), new List { typeof(byte), typeof(char) } }, + { typeof(short), new List { typeof(byte) } } + }; + + /// + /// Try to cast a value to a type, using implicit conversions if possible. + /// + /// The type to cast to. + /// The value to cast. + /// If sucessful, contains the cast value. + /// True if the cast was sucessful, otherwise false. + public static bool TryCast(Type to, object value, out object result) + { + Contract.Requires(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; + } + } +} diff --git a/Perspex.Themes.Default/ButtonStyle.cs b/Perspex.Themes.Default/ButtonStyle.cs index 800fdcc2f1..e711c977e7 100644 --- a/Perspex.Themes.Default/ButtonStyle.cs +++ b/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)), }, }, diff --git a/Tests/Perspex.Base.UnitTests/GlobalSuppressions.cs b/Tests/Perspex.Base.UnitTests/GlobalSuppressions.cs new file mode 100644 index 0000000000..c4e71ed365 --- /dev/null +++ b/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")] \ No newline at end of file diff --git a/Tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/Tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index 8dd0aa9a74..e0ed27530f 100644 --- a/Tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/Tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -78,6 +78,7 @@ + diff --git a/Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs b/Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs index 0ceeaee616..88f14bd565 100644 --- a/Tests/Perspex.Base.UnitTests/PerspexObjectTests.cs +++ b/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(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(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(value); + Assert.Equal(4, value); } [Fact] @@ -616,13 +644,13 @@ namespace Perspex.Base.UnitTests public static readonly PerspexProperty QuxProperty = PerspexProperty.Register("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 FlobProperty = PerspexProperty.Register("Flob"); + public static readonly PerspexProperty FredProperty = + PerspexProperty.Register("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; + } + } } }