瀏覽代碼

Fix type conversions.

They were not correct before. Also clarified the two types of
conversions:

- `TryConvertImplicit` implements the implicit conversions allowed by
the C# language
- `TryConvert` tries every means at its disposal to convert a value to a
type

`AvaloniaObject` uses only implicit conversions. This allows one to
write:

```
var control = new TextBlock
{
[Canvas.TopProperty] = 10
}
```

Without implicit conversions, this would fail because `Canvas.Top` is a
`double` whereas `10` is an `int`, however only implicit conversions
should be used here, otherwise the following would pass, which is
probably not what would be wanted:

```
var control = new TextBlock
{
[TextBlock.TextProperty] = observable, // Text is now the type name of
`observable`
}
```

`DefaultValueConverter` uses `TryConvert`, i.e. every conversion
possible.

Fixes #972.
Steven Kirk 8 年之前
父節點
當前提交
c9e90fd7fc

+ 3 - 3
src/Avalonia.Base/AvaloniaObject.cs

@@ -578,13 +578,13 @@ namespace Avalonia
 
             if (notification == null)
             {
-                return TypeUtilities.CastOrDefault(value, type);
+                return TypeUtilities.ConvertImplicitOrDefault(value, type);
             }
             else
             {
                 if (notification.HasValue)
                 {
-                    notification.SetValue(TypeUtilities.CastOrDefault(notification.Value, type));
+                    notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type));
                 }
 
                 return notification;
@@ -735,7 +735,7 @@ namespace Avalonia
                 ThrowNotRegistered(property);
             }
 
-            if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
+            if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value))
             {
                 throw new ArgumentException(string.Format(
                     "Invalid value for Property '{0}': '{1}' ({2})",

+ 1 - 1
src/Avalonia.Base/AvaloniaProperty.cs

@@ -476,7 +476,7 @@ namespace Avalonia
         /// <returns>True if the value is valid, otherwise false.</returns>
         public bool IsValidValue(object value)
         {
-            return TypeUtilities.TryCast(PropertyType, value, out value);
+            return TypeUtilities.TryConvertImplicit(PropertyType, value, out value);
         }
 
         /// <summary>

+ 1 - 1
src/Avalonia.Base/PriorityValue.cs

@@ -249,7 +249,7 @@ namespace Avalonia
                 value = (notification.HasValue) ? notification.Value : null;
             }
 
-            if (TypeUtilities.TryCast(_valueType, value, out castValue))
+            if (TypeUtilities.TryConvertImplicit(_valueType, value, out castValue))
             {
                 var old = _value;
 

+ 162 - 75
src/Avalonia.Base/Utilities/TypeUtilities.cs

@@ -14,17 +14,61 @@ namespace Avalonia.Utilities
     /// </summary>
     public static class TypeUtilities
     {
-        private static readonly Dictionary<Type, List<Type>> Conversions = new Dictionary<Type, List<Type>>()
+        private static int[] Conversions =
         {
-            { 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) } }
+            0b101111111111101, // Boolean
+            0b100001111111110, // Char
+            0b101111111111111, // SByte
+            0b101111111111111, // Byte
+            0b101111111111111, // Int16
+            0b101111111111111, // UInt16
+            0b101111111111111, // Int32
+            0b101111111111111, // UInt32
+            0b101111111111111, // Int64
+            0b101111111111111, // UInt64
+            0b101111111111101, // Single
+            0b101111111111101, // Double
+            0b101111111111101, // Decimal
+            0b110000000000000, // DateTime
+            0b111111111111111, // String
+        };
+
+        private static int[] ImplicitConversions =
+        {
+            0b000000000000001, // Boolean
+            0b001110111100010, // Char
+            0b001110101010100, // SByte
+            0b001111111111000, // Byte
+            0b001110101010000, // Int16
+            0b001111111100000, // UInt16
+            0b001110101000000, // Int32
+            0b001111110000000, // UInt32
+            0b001110100000000, // Int64
+            0b001111000000000, // UInt64
+            0b000110000000000, // Single
+            0b000100000000000, // Double
+            0b001000000000000, // Decimal
+            0b010000000000000, // DateTime
+            0b100000000000000, // String
+        };
+
+        private static Type[] InbuiltTypes =
+        {
+            typeof(Boolean),
+            typeof(Char),
+            typeof(SByte),
+            typeof(Byte),
+            typeof(Int16),
+            typeof(UInt16),
+            typeof(Int32),
+            typeof(UInt32),
+            typeof(Int64),
+            typeof(UInt64),
+            typeof(Single),
+            typeof(Double),
+            typeof(Decimal),
+            typeof(DateTime),
+            typeof(String),
         };
 
         private static readonly Type[] NumericTypes = new[]
@@ -54,49 +98,104 @@ namespace Avalonia.Utilities
         }
 
         /// <summary>
-        /// Try to cast a value to a type, using implicit conversions if possible.
+        /// Try to convert a value to a type by any means possible.
         /// </summary>
         /// <param name="to">The type to cast to.</param>
         /// <param name="value">The value to cast.</param>
+        /// <param name="culture">The culture to use.</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)
+        public static bool TryConvert(Type to, object value, CultureInfo culture, out object result)
         {
-            Contract.Requires<ArgumentNullException>(to != null);
-
             if (value == null)
             {
                 result = null;
                 return AcceptsNull(to);
             }
 
-            var from = value.GetType();
-
             if (value == AvaloniaProperty.UnsetValue)
             {
                 result = value;
                 return true;
             }
-            else if (to.GetTypeInfo().IsAssignableFrom(from.GetTypeInfo()))
+
+            var from = value.GetType();
+            var fromTypeInfo = from.GetTypeInfo();
+            var toTypeInfo = to.GetTypeInfo();
+
+            if (toTypeInfo.IsAssignableFrom(fromTypeInfo))
             {
                 result = value;
                 return true;
             }
-            else if (Conversions.ContainsKey(to) && Conversions[to].Contains(from))
+
+            if (to == typeof(string))
             {
-                result = Convert.ChangeType(value, to);
+                result = Convert.ToString(value);
                 return true;
             }
-            else
+
+            if (toTypeInfo.IsEnum && from == typeof(string))
+            {
+                if (Enum.IsDefined(to, (string)value))
+                {
+                    result = Enum.Parse(to, (string)value);
+                    return true;
+                }
+            }
+
+            if (!fromTypeInfo.IsEnum && toTypeInfo.IsEnum)
             {
-                var cast = from.GetRuntimeMethods()
-                    .FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == to);
+                result = null;
+
+                if (TryConvert(Enum.GetUnderlyingType(to), value, culture, out object enumValue))
+                {
+                    result = Enum.ToObject(to, enumValue);
+                    return true;
+                }
+            }
 
-                if (cast != null)
+            if (fromTypeInfo.IsEnum && IsNumeric(to))
+            {
+                try
                 {
-                    result = cast.Invoke(null, new[] { value });
+                    result = Convert.ChangeType((int)value, to, culture);
                     return true;
                 }
+                catch
+                {
+                    result = null;
+                    return false;
+                }
+            }
+
+            var convertableFrom = Array.IndexOf(InbuiltTypes, from);
+            var convertableTo = Array.IndexOf(InbuiltTypes, to);
+
+            if (convertableFrom != -1 && convertableTo != -1)
+            {
+                if ((Conversions[convertableFrom] & 1 << convertableTo) != 0)
+                {
+                    try
+                    {
+                        result = Convert.ChangeType(value, to, culture);
+                        return true;
+                    }
+                    catch
+                    {
+                        result = null;
+                        return false;
+                    }
+                }
+            }
+
+            var cast = from.GetRuntimeMethods()
+                .FirstOrDefault(m => (m.Name == "op_Implicit" || m.Name == "op_Explicit") && m.ReturnType == to);
+
+            if (cast != null)
+            {
+                result = cast.Invoke(null, new[] { value });
+                return true;
             }
 
             result = null;
@@ -104,15 +203,14 @@ namespace Avalonia.Utilities
         }
 
         /// <summary>
-        /// Try to convert a value to a type, using <see cref="System.Convert"/> if possible,
-        /// otherwise using <see cref="TryCast(Type, object, out object)"/>.
+        /// Try to convert a value to a type using the implicit conversions allowed by the C#
+        /// language.
         /// </summary>
         /// <param name="to">The type to cast to.</param>
         /// <param name="value">The value to cast.</param>
-        /// <param name="culture">The culture to use.</param>
         /// <param name="result">If sucessful, contains the cast value.</param>
         /// <returns>True if the cast was sucessful, otherwise false.</returns>
-        public static bool TryConvert(Type to, object value, CultureInfo culture, out object result)
+        public static bool TryConvertImplicit(Type to, object value, out object result)
         {
             if (value == null)
             {
@@ -120,54 +218,44 @@ namespace Avalonia.Utilities
                 return AcceptsNull(to);
             }
 
-            var from = value.GetType();
-
             if (value == AvaloniaProperty.UnsetValue)
             {
                 result = value;
                 return true;
             }
 
-            if (to.GetTypeInfo().IsAssignableFrom(from.GetTypeInfo()))
-            {
-                result = value;
-                return true;
-            }
+            var from = value.GetType();
+            var fromTypeInfo = from.GetTypeInfo();
+            var toTypeInfo = to.GetTypeInfo();
 
-            if (to == typeof(string))
+            if (toTypeInfo.IsAssignableFrom(fromTypeInfo))
             {
-                result = Convert.ToString(value);
+                result = value;
                 return true;
             }
 
-            if (to.GetTypeInfo().IsEnum && from == typeof(string))
-            {
-                if (Enum.IsDefined(to, (string)value))
-                {
-                    result = Enum.Parse(to, (string)value);
-                    return true;
-                }
-            }
-
-            bool containsFrom = Conversions.ContainsKey(from);
-            bool containsTo = Conversions.ContainsKey(to);
+            var convertableFrom = Array.IndexOf(InbuiltTypes, from);
+            var convertableTo = Array.IndexOf(InbuiltTypes, to);
 
-            if ((containsFrom && containsTo) || (from == typeof(string) && containsTo))
+            if (convertableFrom != -1 && convertableTo != -1)
             {
-                try
-                {
-                    result = Convert.ChangeType(value, to, culture);
-                    return true;
-                }
-                catch
+                if ((ImplicitConversions[convertableFrom] & 1 << convertableTo) != 0)
                 {
-                    result = null;
-                    return false;
+                    try
+                    {
+                        result = Convert.ChangeType(value, to, CultureInfo.InvariantCulture);
+                        return true;
+                    }
+                    catch
+                    {
+                        result = null;
+                        return false;
+                    }
                 }
             }
 
             var cast = from.GetRuntimeMethods()
-                .FirstOrDefault(m => (m.Name == "op_Implicit" || m.Name == "op_Explicit") && m.ReturnType == to);
+                .FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == to);
 
             if (cast != null)
             {
@@ -180,29 +268,28 @@ namespace Avalonia.Utilities
         }
 
         /// <summary>
-        /// Casts a value to a type, returning the default for that type if the value could not be
-        /// cast.
+        /// Convert a value to a type by any means possible, returning the default for that type
+        /// if the value could not be converted.
         /// </summary>
         /// <param name="value">The value to cast.</param>
         /// <param name="type">The type to cast to..</param>
+        /// <param name="culture">The culture to use.</param>
         /// <returns>A value of <paramref name="type"/>.</returns>
-        public static object CastOrDefault(object value, Type type)
+        public static object ConvertOrDefault(object value, Type type, CultureInfo culture)
         {
-            var typeInfo = type.GetTypeInfo();
-            object result;
+            return TryConvert(type, value, culture, out object result) ? result : Default(type);
+        }
 
-            if (TypeUtilities.TryCast(type, value, out result))
-            {
-                return result;
-            }
-            else if (typeInfo.IsValueType)
-            {
-                return Activator.CreateInstance(type);
-            }
-            else
-            {
-                return null;
-            }
+        /// <summary>
+        /// Convert a value to a type using the implicit conversions allowed by the C# language or
+        /// return the default for the type if the value could not be converted.
+        /// </summary>
+        /// <param name="value">The value to cast.</param>
+        /// <param name="type">The type to cast to..</param>
+        /// <returns>A value of <paramref name="type"/>.</returns>
+        public static object ConvertImplicitOrDefault(object value, Type type)
+        {
+            return TryConvertImplicit(type, value, out object result) ? result : Default(type);
         }
 
         /// <summary>

+ 14 - 50
src/Markup/Avalonia.Markup/DefaultValueConverter.cs

@@ -3,10 +3,7 @@
 
 using System;
 using System.Globalization;
-using System.Linq;
-using System.Reflection;
 using Avalonia.Data;
-using Avalonia.Logging;
 using Avalonia.Utilities;
 
 namespace Avalonia.Markup
@@ -32,32 +29,28 @@ namespace Avalonia.Markup
         /// <returns>The converted value.</returns>
         public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
-            object result;
-
-            if (value != null && 
-                (TypeUtilities.TryConvert(targetType, value, culture, out result) ||
-                 TryConvertEnum(value, targetType, culture, out result)))
+            if (value == null)
             {
-                return result;
+                return AvaloniaProperty.UnsetValue;
             }
 
-            if (value != null)
+            if (TypeUtilities.TryConvert(targetType, value, culture, out object result))
             {
-                string message;
+                return result;
+            }
 
-                if (TypeUtilities.IsNumeric(targetType))
-                {
-                    message = $"'{value}' is not a valid number.";
-                }
-                else
-                {
-                    message = $"Could not convert '{value}' to '{targetType.Name}'.";
-                }
+            string message;
 
-                return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error);
+            if (TypeUtilities.IsNumeric(targetType))
+            {
+                message = $"'{value}' is not a valid number.";
+            }
+            else
+            {
+                message = $"Could not convert '{value}' to '{targetType.Name}'.";
             }
 
-            return AvaloniaProperty.UnsetValue;
+            return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error);
         }
 
         /// <summary>
@@ -72,34 +65,5 @@ namespace Avalonia.Markup
         {
             return Convert(value, targetType, parameter, culture);
         }
-
-        private bool TryConvertEnum(object value, Type targetType, CultureInfo cultur, out object result)
-        {
-            var valueTypeInfo = value.GetType().GetTypeInfo();
-            var targetTypeInfo = targetType.GetTypeInfo();
-
-            if (valueTypeInfo.IsEnum && !targetTypeInfo.IsEnum)
-            {
-                var enumValue = (int)value;
-
-                if (TypeUtilities.TryCast(targetType, enumValue, out result))
-                {
-                    return true;
-                }
-            }
-            else if (!valueTypeInfo.IsEnum && targetTypeInfo.IsEnum)
-            {
-                object intValue;
-
-                if (TypeUtilities.TryCast(typeof(int), value, out intValue))
-                {
-                    result = Enum.ToObject(targetType, intValue);
-                    return true;
-                }
-            }
-
-            result = null;
-            return false;
-        }
     }
 }

+ 17 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Parsers/SelectorParserTests.cs

@@ -0,0 +1,17 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml.Parsers;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Parsers
+{
+    public class SelectorParserTests
+    {
+        [Fact]
+        public void Parses_Boolean_Property_Selector()
+        {
+            var target = new SelectorParser((type, ns) => typeof(TextBlock));
+            var result = target.Parse("TextBlock[IsPointerOver=True]");
+        }
+    }
+}