Browse Source

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

Fix type conversions.
pull/977/head
Steven Kirk 9 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)
{
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})",

2
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>

2
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;

237
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>

64
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
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