Browse Source

Merge pull request #974 from AvaloniaUI/fixes/972-conversions

Fix type conversions.
pull/977/head
Steven Kirk 10 years ago
committed by GitHub
parent
commit
4fa7126d30
  1. 6
      src/Avalonia.Base/AvaloniaObject.cs
  2. 2
      src/Avalonia.Base/AvaloniaProperty.cs
  3. 2
      src/Avalonia.Base/PriorityValue.cs
  4. 237
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  5. 64
      src/Markup/Avalonia.Markup/DefaultValueConverter.cs
  6. 17
      tests/Avalonia.Markup.Xaml.UnitTests/Parsers/SelectorParserTests.cs

6
src/Avalonia.Base/AvaloniaObject.cs

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

2
src/Avalonia.Base/AvaloniaProperty.cs

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

2
src/Avalonia.Base/PriorityValue.cs

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

237
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -14,17 +14,61 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
public static class TypeUtilities 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) } }, 0b101111111111101, // Boolean
{ 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) } }, 0b100001111111110, // Char
{ 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) } }, 0b101111111111111, // SByte
{ typeof(ulong), new List<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } }, 0b101111111111111, // Byte
{ typeof(long), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } }, 0b101111111111111, // Int16
{ typeof(uint), new List<Type> { typeof(byte), typeof(ushort), typeof(char) } }, 0b101111111111111, // UInt16
{ typeof(int), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } }, 0b101111111111111, // Int32
{ typeof(ushort), new List<Type> { typeof(byte), typeof(char) } }, 0b101111111111111, // UInt32
{ typeof(short), new List<Type> { typeof(byte) } } 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[] private static readonly Type[] NumericTypes = new[]
@ -54,49 +98,104 @@ namespace Avalonia.Utilities
} }
/// <summary> /// <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> /// </summary>
/// <param name="to">The type to cast to.</param> /// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</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> /// <param name="result">If sucessful, contains the cast value.</param>
/// <returns>True if the cast was sucessful, otherwise false.</returns> /// <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) if (value == null)
{ {
result = null; result = null;
return AcceptsNull(to); return AcceptsNull(to);
} }
var from = value.GetType();
if (value == AvaloniaProperty.UnsetValue) if (value == AvaloniaProperty.UnsetValue)
{ {
result = value; result = value;
return true; 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; result = value;
return true; 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; 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() result = null;
.FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == to);
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; 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; result = null;
@ -104,15 +203,14 @@ namespace Avalonia.Utilities
} }
/// <summary> /// <summary>
/// Try to convert a value to a type, using <see cref="System.Convert"/> if possible, /// Try to convert a value to a type using the implicit conversions allowed by the C#
/// otherwise using <see cref="TryCast(Type, object, out object)"/>. /// language.
/// </summary> /// </summary>
/// <param name="to">The type to cast to.</param> /// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</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> /// <param name="result">If sucessful, contains the cast value.</param>
/// <returns>True if the cast was sucessful, otherwise false.</returns> /// <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) if (value == null)
{ {
@ -120,54 +218,44 @@ namespace Avalonia.Utilities
return AcceptsNull(to); return AcceptsNull(to);
} }
var from = value.GetType();
if (value == AvaloniaProperty.UnsetValue) if (value == AvaloniaProperty.UnsetValue)
{ {
result = value; result = value;
return true; return true;
} }
if (to.GetTypeInfo().IsAssignableFrom(from.GetTypeInfo())) var from = value.GetType();
{ var fromTypeInfo = from.GetTypeInfo();
result = value; var toTypeInfo = to.GetTypeInfo();
return true;
}
if (to == typeof(string)) if (toTypeInfo.IsAssignableFrom(fromTypeInfo))
{ {
result = Convert.ToString(value); result = value;
return true; return true;
} }
if (to.GetTypeInfo().IsEnum && from == typeof(string)) var convertableFrom = Array.IndexOf(InbuiltTypes, from);
{ var convertableTo = Array.IndexOf(InbuiltTypes, to);
if (Enum.IsDefined(to, (string)value))
{
result = Enum.Parse(to, (string)value);
return true;
}
}
bool containsFrom = Conversions.ContainsKey(from);
bool containsTo = Conversions.ContainsKey(to);
if ((containsFrom && containsTo) || (from == typeof(string) && containsTo)) if (convertableFrom != -1 && convertableTo != -1)
{ {
try if ((ImplicitConversions[convertableFrom] & 1 << convertableTo) != 0)
{
result = Convert.ChangeType(value, to, culture);
return true;
}
catch
{ {
result = null; try
return false; {
result = Convert.ChangeType(value, to, CultureInfo.InvariantCulture);
return true;
}
catch
{
result = null;
return false;
}
} }
} }
var cast = from.GetRuntimeMethods() 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) if (cast != null)
{ {
@ -180,29 +268,28 @@ namespace Avalonia.Utilities
} }
/// <summary> /// <summary>
/// Casts a value to a type, returning the default for that type if the value could not be /// Convert a value to a type by any means possible, returning the default for that type
/// cast. /// if the value could not be converted.
/// </summary> /// </summary>
/// <param name="value">The value to cast.</param> /// <param name="value">The value to cast.</param>
/// <param name="type">The type to cast to..</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> /// <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(); return TryConvert(type, value, culture, out object result) ? result : Default(type);
object result; }
if (TypeUtilities.TryCast(type, value, out result)) /// <summary>
{ /// Convert a value to a type using the implicit conversions allowed by the C# language or
return result; /// return the default for the type if the value could not be converted.
} /// </summary>
else if (typeInfo.IsValueType) /// <param name="value">The value to cast.</param>
{ /// <param name="type">The type to cast to..</param>
return Activator.CreateInstance(type); /// <returns>A value of <paramref name="type"/>.</returns>
} public static object ConvertImplicitOrDefault(object value, Type type)
else {
{ return TryConvertImplicit(type, value, out object result) ? result : Default(type);
return null;
}
} }
/// <summary> /// <summary>

64
src/Markup/Avalonia.Markup/DefaultValueConverter.cs

@ -3,10 +3,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Reflection;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Markup namespace Avalonia.Markup
@ -32,32 +29,28 @@ namespace Avalonia.Markup
/// <returns>The converted value.</returns> /// <returns>The converted value.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
object result; if (value == null)
if (value != null &&
(TypeUtilities.TryConvert(targetType, value, culture, out result) ||
TryConvertEnum(value, targetType, culture, out result)))
{ {
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)) string message;
{
message = $"'{value}' is not a valid number.";
}
else
{
message = $"Could not convert '{value}' to '{targetType.Name}'.";
}
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> /// <summary>
@ -72,34 +65,5 @@ namespace Avalonia.Markup
{ {
return Convert(value, targetType, parameter, culture); 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
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]");
}
}
}
Loading…
Cancel
Save