Add APIs for conversions

This change adds API surface for performing type conversions in the same
way that `@bind` will perform them. This allows us to centralize these
implementations where it makes sense (form controls).

More critically we need to present a uniform API surface for the
compiler to use in code generation for converting values to strings
using the appropriate culture. Surfacing an API for this and using it
from the compiler is the missing piece for `@bind` and globalization
(number, date).
This commit is contained in:
Ryan Nowak 2019-07-04 17:31:19 -07:00 committed by Ryan Nowak
parent a5411de678
commit c5cf99f6a2
10 changed files with 1808 additions and 674 deletions

View File

@ -25,6 +25,53 @@ namespace Microsoft.AspNetCore.Components
public static partial class BindAttributes
{
}
public static partial class BindConverter
{
public static bool FormatValue(bool value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(System.DateTime value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(System.DateTime value, string format, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(System.DateTimeOffset value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(System.DateTimeOffset value, string format, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(decimal value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(double value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(int value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(long value, System.Globalization.CultureInfo culture = null) { throw null; }
public static bool? FormatValue(bool? value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(System.DateTimeOffset? value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(System.DateTimeOffset? value, string format, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(System.DateTime? value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(System.DateTime? value, string format, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(decimal? value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(double? value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(int? value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(long? value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(float? value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(float value, System.Globalization.CultureInfo culture = null) { throw null; }
public static string FormatValue(string value, System.Globalization.CultureInfo culture = null) { throw null; }
public static object FormatValue<T>(T value, System.Globalization.CultureInfo culture = null) { throw null; }
public static bool TryConvertToBool(object obj, System.Globalization.CultureInfo culture, out bool value) { throw null; }
public static bool TryConvertToDateTime(object obj, System.Globalization.CultureInfo culture, out System.DateTime value) { throw null; }
public static bool TryConvertToDateTime(object obj, System.Globalization.CultureInfo culture, string format, out System.DateTime value) { throw null; }
public static bool TryConvertToDateTimeOffset(object obj, System.Globalization.CultureInfo culture, out System.DateTimeOffset value) { throw null; }
public static bool TryConvertToDateTimeOffset(object obj, System.Globalization.CultureInfo culture, string format, out System.DateTimeOffset value) { throw null; }
public static bool TryConvertToDecimal(object obj, System.Globalization.CultureInfo culture, out decimal value) { throw null; }
public static bool TryConvertToDouble(object obj, System.Globalization.CultureInfo culture, out double value) { throw null; }
public static bool TryConvertToFloat(object obj, System.Globalization.CultureInfo culture, out float value) { throw null; }
public static bool TryConvertToInt(object obj, System.Globalization.CultureInfo culture, out int value) { throw null; }
public static bool TryConvertToLong(object obj, System.Globalization.CultureInfo culture, out long value) { throw null; }
public static bool TryConvertToNullableBool(object obj, System.Globalization.CultureInfo culture, out bool? value) { throw null; }
public static bool TryConvertToNullableDateTime(object obj, System.Globalization.CultureInfo culture, out System.DateTime? value) { throw null; }
public static bool TryConvertToNullableDateTime(object obj, System.Globalization.CultureInfo culture, string format, out System.DateTime? value) { throw null; }
public static bool TryConvertToNullableDateTimeOffset(object obj, System.Globalization.CultureInfo culture, out System.DateTimeOffset? value) { throw null; }
public static bool TryConvertToNullableDateTimeOffset(object obj, System.Globalization.CultureInfo culture, string format, out System.DateTimeOffset? value) { throw null; }
public static bool TryConvertToNullableDecimal(object obj, System.Globalization.CultureInfo culture, out decimal? value) { throw null; }
public static bool TryConvertToNullableDouble(object obj, System.Globalization.CultureInfo culture, out double? value) { throw null; }
public static bool TryConvertToNullableFloat(object obj, System.Globalization.CultureInfo culture, out float? value) { throw null; }
public static bool TryConvertToNullableInt(object obj, System.Globalization.CultureInfo culture, out int? value) { throw null; }
public static bool TryConvertToNullableLong(object obj, System.Globalization.CultureInfo culture, out long? value) { throw null; }
public static bool TryConvertToString(object obj, System.Globalization.CultureInfo culture, out string value) { throw null; }
public static bool TryConvertTo<T>(object obj, System.Globalization.CultureInfo culture, out T value) { throw null; }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true, Inherited=true)]
public sealed partial class BindElementAttribute : System.Attribute
{

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using static Microsoft.AspNetCore.Components.BindConverter;
namespace Microsoft.AspNetCore.Components
{
@ -24,406 +22,6 @@ namespace Microsoft.AspNetCore.Components
// For now we're not necessarily handling this correctly since we parse the same way for number and text.
public static class EventCallbackFactoryBinderExtensions
{
private delegate bool BindConverter<T>(object obj, CultureInfo culture, out T value);
private delegate bool BindConverterWithFormat<T>(object obj, CultureInfo culture, string format, out T value);
// Perf: conversion delegates are written as static funcs so we can prevent
// allocations for these simple cases.
private readonly static BindConverter<string> ConvertToString = ConvertToStringCore;
private static bool ConvertToStringCore(object obj, CultureInfo culture, out string value)
{
// We expect the input to already be a string.
value = (string)obj;
return true;
}
private static BindConverter<bool> ConvertToBool = ConvertToBoolCore;
private static BindConverter<bool?> ConvertToNullableBool = ConvertToNullableBoolCore;
private static bool ConvertToBoolCore(object obj, CultureInfo culture, out bool value)
{
// We expect the input to already be a bool.
value = (bool)obj;
return true;
}
private static bool ConvertToNullableBoolCore(object obj, CultureInfo culture, out bool? value)
{
// We expect the input to already be a bool.
value = (bool?)obj;
return true;
}
private static BindConverter<int> ConvertToInt = ConvertToIntCore;
private static BindConverter<int?> ConvertToNullableInt = ConvertToNullableIntCore;
private static bool ConvertToIntCore(object obj, CultureInfo culture, out int value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (!int.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableIntCore(object obj, CultureInfo culture, out int? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!int.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static BindConverter<long> ConvertToLong = ConvertToLongCore;
private static BindConverter<long?> ConvertToNullableLong = ConvertToNullableLongCore;
private static bool ConvertToLongCore(object obj, CultureInfo culture, out long value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (!long.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableLongCore(object obj, CultureInfo culture, out long? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!long.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static BindConverter<float> ConvertToFloat = ConvertToFloatCore;
private static BindConverter<float?> ConvertToNullableFloat = ConvertToNullableFloatCore;
private static bool ConvertToFloatCore(object obj, CultureInfo culture, out float value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (!float.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableFloatCore(object obj, CultureInfo culture, out float? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!float.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static BindConverter<double> ConvertToDouble = ConvertToDoubleCore;
private static BindConverter<double?> ConvertToNullableDouble = ConvertToNullableDoubleCore;
private static bool ConvertToDoubleCore(object obj, CultureInfo culture, out double value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (!double.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableDoubleCore(object obj, CultureInfo culture, out double? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!double.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static BindConverter<decimal> ConvertToDecimal = ConvertToDecimalCore;
private static BindConverter<decimal?> ConvertToNullableDecimal = ConvertToNullableDecimalCore;
private static bool ConvertToDecimalCore(object obj, CultureInfo culture, out decimal value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (!decimal.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableDecimalCore(object obj, CultureInfo culture, out decimal? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!decimal.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static BindConverter<DateTime> ConvertToDateTime = ConvertToDateTimeCore;
private static BindConverterWithFormat<DateTime> ConvertToDateTimeWithFormat = ConvertToDateTimeCore;
private static BindConverter<DateTime?> ConvertToNullableDateTime = ConvertToNullableDateTimeCore;
private static BindConverterWithFormat<DateTime?> ConvertToNullableDateTimeWithFormat = ConvertToNullableDateTimeCore;
private static bool ConvertToDateTimeCore(object obj, CultureInfo culture, out DateTime value)
{
return ConvertToDateTimeCore(obj, culture, format: null, out value);
}
private static bool ConvertToDateTimeCore(object obj, CultureInfo culture, string format, out DateTime value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (format != null && DateTime.TryParseExact(text, format, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
{
value = converted;
return true;
}
else if (format == null && DateTime.TryParse(text, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out converted))
{
value = converted;
return true;
}
value = default;
return false;
}
private static bool ConvertToNullableDateTimeCore(object obj, CultureInfo culture, out DateTime? value)
{
return ConvertToNullableDateTimeCore(obj, culture, format: null, out value);
}
private static bool ConvertToNullableDateTimeCore(object obj, CultureInfo culture, string format, out DateTime? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (format != null && DateTime.TryParseExact(text, format, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
{
value = converted;
return true;
}
else if (format == null && DateTime.TryParse(text, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out converted))
{
value = converted;
return true;
}
value = default;
return false;
}
private static BindConverter<DateTimeOffset> ConvertToDateTimeOffset = ConvertToDateTimeOffsetCore;
private static BindConverterWithFormat<DateTimeOffset> ConvertToDateTimeOffsetWithFormat = ConvertToDateTimeOffsetCore;
private static BindConverter<DateTimeOffset?> ConvertToNullableDateTimeOffset = ConvertToNullableDateTimeOffsetCore;
private static BindConverterWithFormat<DateTimeOffset?> ConvertToNullableDateTimeOffsetWithFormat = ConvertToNullableDateTimeOffsetCore;
private static bool ConvertToDateTimeOffsetCore(object obj, CultureInfo culture, out DateTimeOffset value)
{
return ConvertToDateTimeOffsetCore(obj, culture, format: null, out value);
}
private static bool ConvertToDateTimeOffsetCore(object obj, CultureInfo culture, string format, out DateTimeOffset value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (format != null && DateTimeOffset.TryParseExact(text, format, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
{
value = converted;
return true;
}
else if (format == null && DateTimeOffset.TryParse(text, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out converted))
{
value = converted;
return true;
}
value = default;
return false;
}
private static bool ConvertToNullableDateTimeOffsetCore(object obj, CultureInfo culture, out DateTimeOffset? value)
{
return ConvertToNullableDateTimeOffsetCore(obj, culture, format: null, out value);
}
private static bool ConvertToNullableDateTimeOffsetCore(object obj, CultureInfo culture, string format, out DateTimeOffset? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (format != null && DateTimeOffset.TryParseExact(text, format, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
{
value = converted;
return true;
}
else if (format == null && DateTimeOffset.TryParse(text, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out converted))
{
value = converted;
return true;
}
value = default;
return false;
}
private static bool ConvertToEnum<T>(object obj, CultureInfo culture, out T value) where T : struct, Enum
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!Enum.TryParse<T>(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableEnum<T>(object obj, CultureInfo culture, out T? value) where T : struct, Enum
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!Enum.TryParse<T>(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
/// <summary>
/// For internal use only.
/// </summary>
@ -611,7 +209,7 @@ namespace Microsoft.AspNetCore.Components
double existingValue,
CultureInfo culture = null)
{
return CreateBinderCore<double>(factory, receiver, setter, culture, ConvertToDouble);
return CreateBinderCore<double>(factory, receiver, setter, culture, ConvertToDoubleDelegate);
}
/// <summary>
@ -630,7 +228,7 @@ namespace Microsoft.AspNetCore.Components
double? existingValue,
CultureInfo culture = null)
{
return CreateBinderCore<double?>(factory, receiver, setter, culture, ConvertToNullableDouble);
return CreateBinderCore<double?>(factory, receiver, setter, culture, ConvertToNullableDoubleDelegate);
}
/// <summary>
@ -687,7 +285,7 @@ namespace Microsoft.AspNetCore.Components
DateTime existingValue,
CultureInfo culture = null)
{
return CreateBinderCore<DateTime>(factory, receiver, setter, culture, format: null, ConvertToDateTimeWithFormat);
return CreateBinderCore<DateTime>(factory, receiver, setter, culture, ConvertToDateTime);
}
/// <summary>
@ -727,7 +325,7 @@ namespace Microsoft.AspNetCore.Components
DateTime? existingValue,
CultureInfo culture = null)
{
return CreateBinderCore<DateTime?>(factory, receiver, setter, culture, format: null, ConvertToNullableDateTimeWithFormat);
return CreateBinderCore<DateTime?>(factory, receiver, setter, culture, ConvertToNullableDateTime);
}
/// <summary>
@ -767,7 +365,7 @@ namespace Microsoft.AspNetCore.Components
DateTimeOffset existingValue,
CultureInfo culture = null)
{
return CreateBinderCore<DateTimeOffset>(factory, receiver, setter, culture, format: null, ConvertToDateTimeOffsetWithFormat);
return CreateBinderCore<DateTimeOffset>(factory, receiver, setter, culture, ConvertToDateTimeOffset);
}
/// <summary>
@ -807,7 +405,7 @@ namespace Microsoft.AspNetCore.Components
DateTimeOffset? existingValue,
CultureInfo culture = null)
{
return CreateBinderCore<DateTimeOffset?>(factory, receiver, setter, culture, format: null, ConvertToNullableDateTimeOffsetWithFormat);
return CreateBinderCore<DateTimeOffset?>(factory, receiver, setter, culture, ConvertToNullableDateTimeOffset);
}
/// <summary>
@ -848,7 +446,7 @@ namespace Microsoft.AspNetCore.Components
T existingValue,
CultureInfo culture = null)
{
return CreateBinderCore<T>(factory, receiver, setter, culture, BinderConverterCache.Get<T>());
return CreateBinderCore<T>(factory, receiver, setter, culture, ParserDelegateCache.Get<T>());
}
private static EventCallback<UIChangeEventArgs> CreateBinderCore<T>(
@ -856,7 +454,7 @@ namespace Microsoft.AspNetCore.Components
object receiver,
Action<T> setter,
CultureInfo culture,
BindConverter<T> converter)
BindConverter.BindParser<T> converter)
{
Action<UIChangeEventArgs> callback = e =>
{
@ -900,7 +498,7 @@ namespace Microsoft.AspNetCore.Components
Action<T> setter,
CultureInfo culture,
string format,
BindConverterWithFormat<T> converter)
BindConverter.BindParserWithFormat<T> converter)
{
Action<UIChangeEventArgs> callback = e =>
{
@ -937,147 +535,5 @@ namespace Microsoft.AspNetCore.Components
};
return factory.Create<UIChangeEventArgs>(receiver, callback);
}
// We can't rely on generics + static to cache here unfortunately. That would require us to overload
// CreateBinder on T : struct AND T : class, which is not allowed.
private static class BinderConverterCache
{
private readonly static ConcurrentDictionary<Type, Delegate> _cache = new ConcurrentDictionary<Type, Delegate>();
private static MethodInfo _convertToEnum;
private static MethodInfo _convertToNullableEnum;
public static BindConverter<T> Get<T>()
{
if (!_cache.TryGetValue(typeof(T), out var converter))
{
// We need to replicate all of the primitive cases that we handle here so that they will behave the same way.
// The result will be cached.
if (typeof(T) == typeof(string))
{
converter = ConvertToString;
}
else if (typeof(T) == typeof(bool))
{
converter = ConvertToBool;
}
else if (typeof(T) == typeof(bool?))
{
converter = ConvertToNullableBool;
}
else if (typeof(T) == typeof(int))
{
converter = ConvertToInt;
}
else if (typeof(T) == typeof(int?))
{
converter = ConvertToNullableInt;
}
else if (typeof(T) == typeof(long))
{
converter = ConvertToLong;
}
else if (typeof(T) == typeof(long?))
{
converter = ConvertToNullableLong;
}
else if (typeof(T) == typeof(float))
{
converter = ConvertToFloat;
}
else if (typeof(T) == typeof(float?))
{
converter = ConvertToNullableFloat;
}
else if (typeof(T) == typeof(double))
{
converter = ConvertToDouble;
}
else if (typeof(T) == typeof(double?))
{
converter = ConvertToNullableDouble;
}
else if (typeof(T) == typeof(decimal))
{
converter = ConvertToDecimal;
}
else if (typeof(T) == typeof(decimal?))
{
converter = ConvertToNullableDecimal;
}
else if (typeof(T) == typeof(DateTime))
{
converter = ConvertToDateTime;
}
else if (typeof(T) == typeof(DateTime?))
{
converter = ConvertToNullableDateTime;
}
else if (typeof(T) == typeof(DateTimeOffset))
{
converter = ConvertToDateTimeOffset;
}
else if (typeof(T) == typeof(DateTimeOffset?))
{
converter = ConvertToNullableDateTimeOffset;
}
else if (typeof(T).IsEnum)
{
// We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse.
var method = _convertToEnum ??= typeof(EventCallbackFactoryBinderExtensions).GetMethod(nameof(ConvertToEnum), BindingFlags.NonPublic | BindingFlags.Static);
converter = method.MakeGenericMethod(typeof(T)).CreateDelegate(typeof(BindConverter<T>), target: null);
}
else if (Nullable.GetUnderlyingType(typeof(T)) is Type innerType && innerType.IsEnum)
{
// We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse.
var method = _convertToNullableEnum ??= typeof(EventCallbackFactoryBinderExtensions).GetMethod(nameof(ConvertToNullableEnum), BindingFlags.NonPublic | BindingFlags.Static);
converter = method.MakeGenericMethod(innerType).CreateDelegate(typeof(BindConverter<T>), target: null);
}
else
{
converter = MakeTypeConverterConverter<T>();
}
_cache.TryAdd(typeof(T), converter);
}
return (BindConverter<T>)converter;
}
private static BindConverter<T> MakeTypeConverterConverter<T>()
{
var typeConverter = TypeDescriptor.GetConverter(typeof(T));
if (typeConverter == null || !typeConverter.CanConvertFrom(typeof(string)))
{
throw new InvalidOperationException(
$"The type '{typeof(T).FullName}' does not have an associated {typeof(TypeConverter).Name} that supports " +
$"conversion from a string. " +
$"Apply '{typeof(TypeConverterAttribute).Name}' to the type to register a converter.");
}
return ConvertWithTypeConverter;
bool ConvertWithTypeConverter(object obj, CultureInfo culture, out T value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
// We intentionally close-over the TypeConverter to cache it. The TypeDescriptor infrastructure is slow.
var converted = typeConverter.ConvertFromString(context: null, culture ?? CultureInfo.CurrentCulture, text);
if (converted == null)
{
value = default;
return false;
}
value = (T)converted;
return true;
}
}
}
}
}

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Components.Forms
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "type", "checkbox");
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "checked", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(4, "checked", BindConverter.FormatValue(CurrentValue));
builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder<bool>(this, __value => CurrentValue = __value, CurrentValue));
builder.CloseElement();
}

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Components.Forms
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "type", "date");
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(4, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
builder.CloseElement();
}
@ -38,9 +38,9 @@ namespace Microsoft.AspNetCore.Components.Forms
switch (value)
{
case DateTime dateTimeValue:
return dateTimeValue.ToString(DateFormat, CultureInfo.InvariantCulture);
return BindConverter.FormatValue(dateTimeValue, DateFormat, CultureInfo.InvariantCulture);
case DateTimeOffset dateTimeOffsetValue:
return dateTimeOffsetValue.ToString(DateFormat, CultureInfo.InvariantCulture);
return BindConverter.FormatValue(dateTimeOffsetValue, DateFormat, CultureInfo.InvariantCulture);
default:
return string.Empty; // Handles null for Nullable<DateTime>, etc.
}
@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Components.Forms
static bool TryParseDateTime(string value, out T result)
{
var success = DateTime.TryParseExact(value, DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedValue);
var success = BindConverter.TryConvertToDateTime(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue);
if (success)
{
result = (T)(object)parsedValue;
@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.Components.Forms
static bool TryParseDateTimeOffset(string value, out T result)
{
var success = DateTimeOffset.TryParseExact(value, DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedValue);
var success = BindConverter.TryConvertToDateTimeOffset(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue);
if (success)
{
result = (T)(object)parsedValue;

View File

@ -13,38 +13,18 @@ namespace Microsoft.AspNetCore.Components.Forms
/// </summary>
public class InputNumber<T> : InputBase<T>
{
delegate bool Parser(string value, out T result);
private static Parser _parser;
private static string _stepAttributeValue; // Null by default, so only allows whole numbers as per HTML spec
// Determine the parsing logic once per T and cache it, so we don't have to consider all the possible types on each parse
static InputNumber()
{
// Unwrap Nullable<T>, because InputBase already deals with the Nullable aspect
// of it for us. We will only get asked to parse the T for nonempty inputs.
var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (targetType == typeof(int))
if (targetType == typeof(int) ||
targetType == typeof(float) ||
targetType == typeof(double) ||
targetType == typeof(decimal))
{
_parser = TryParseInt;
}
else if (targetType == typeof(long))
{
_parser = TryParseLong;
}
else if (targetType == typeof(float))
{
_parser = TryParseFloat;
_stepAttributeValue = "any";
}
else if (targetType == typeof(double))
{
_parser = TryParseDouble;
_stepAttributeValue = "any";
}
else if (targetType == typeof(decimal))
{
_parser = TryParseDecimal;
_stepAttributeValue = "any";
}
else
@ -62,11 +42,11 @@ namespace Microsoft.AspNetCore.Components.Forms
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "step", _stepAttributeValue); // Before the splat so the user can override
builder.AddAttribute(1, "step", _stepAttributeValue);
builder.AddMultipleAttributes(2, AdditionalAttributes);
builder.AddAttribute(3, "type", "number");
builder.AddAttribute(4, "class", CssClass);
builder.AddAttribute(5, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(5, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(6, "onchange", EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
builder.CloseElement();
}
@ -74,7 +54,7 @@ namespace Microsoft.AspNetCore.Components.Forms
/// <inheritdoc />
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
{
if (_parser(value, out result))
if (BindConverter.TryConvertTo<T>(value, CultureInfo.InvariantCulture, out result))
{
validationErrorMessage = null;
return true;
@ -100,98 +80,23 @@ namespace Microsoft.AspNetCore.Components.Forms
return null;
case int @int:
return @int.ToString(CultureInfo.InvariantCulture);
return BindConverter.FormatValue(@int, CultureInfo.InvariantCulture);
case long @long:
return @long.ToString(CultureInfo.InvariantCulture);
return BindConverter.FormatValue(@long, CultureInfo.InvariantCulture);
case float @float:
return @float.ToString(CultureInfo.InvariantCulture);
return BindConverter.FormatValue(@float, CultureInfo.InvariantCulture);
case double @double:
return @double.ToString(CultureInfo.InvariantCulture);
return BindConverter.FormatValue(@double, CultureInfo.InvariantCulture);
case decimal @decimal:
return @decimal.ToString(CultureInfo.InvariantCulture);
return BindConverter.FormatValue(@decimal, CultureInfo.InvariantCulture);
default:
throw new InvalidOperationException($"Unsupported type {value.GetType()}");
}
}
static bool TryParseInt(string value, out T result)
{
var success = int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue);
if (success)
{
result = (T)(object)parsedValue;
return true;
}
else
{
result = default;
return false;
}
}
static bool TryParseLong(string value, out T result)
{
var success = long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue);
if (success)
{
result = (T)(object)parsedValue;
return true;
}
else
{
result = default;
return false;
}
}
static bool TryParseFloat(string value, out T result)
{
var success = float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
if (success && !float.IsInfinity(parsedValue))
{
result = (T)(object)parsedValue;
return true;
}
else
{
result = default;
return false;
}
}
static bool TryParseDouble(string value, out T result)
{
var success = double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
if (success && !double.IsInfinity(parsedValue))
{
result = (T)(object)parsedValue;
return true;
}
else
{
result = default;
return false;
}
}
static bool TryParseDecimal(string value, out T result)
{
var success = decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
if (success)
{
result = (T)(object)parsedValue;
return true;
}
else
{
result = default;
return false;
}
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Forms
@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.Components.Forms
builder.OpenElement(0, "select");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
builder.AddContent(5, ChildContent);
builder.CloseElement();
@ -39,14 +40,14 @@ namespace Microsoft.AspNetCore.Components.Forms
}
else if (typeof(T).IsEnum)
{
// There's no non-generic Enum.TryParse (https://github.com/dotnet/corefx/issues/692)
try
var success = BindConverter.TryConvertTo<T>(value, CultureInfo.CurrentCulture, out var parsedValue);
if (success)
{
result = (T)Enum.Parse(typeof(T), value);
result = parsedValue;
validationErrorMessage = null;
return true;
}
catch (ArgumentException)
else
{
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.Forms
builder.OpenElement(0, "input");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue));
builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
builder.CloseElement();
}

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.Forms
builder.OpenElement(0, "textarea");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue));
builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
builder.CloseElement();
}

View File

@ -0,0 +1,307 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using Xunit;
namespace Microsoft.AspNetCore.Components
{
// This is some basic coverage, it's not in depth because there are many many APIs here
// and they mostly call through to CoreFx. We don't want to test the globalization details
// of .NET in detail where we can avoid it.
//
// Instead there's a sampling of things that have somewhat unique behavior or semantics.
public class BindConverterTest
{
[Fact]
public void FormatValue_Bool()
{
// Arrange
var value = true;
var expected = true;
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_Bool_Generic()
{
// Arrange
var value = true;
var expected = true;
// Act
var actual = BindConverter.FormatValue<bool>(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_NullableBool()
{
// Arrange
var value = (bool?)true;
var expected = true;
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_NullableBool_Generic()
{
// Arrange
var value = true;
var expected = true;
// Act
var actual = BindConverter.FormatValue<bool?>(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_NullableBoolNull()
{
// Arrange
var value = (bool?)null;
var expected = (bool?)null;
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_NullableBoolNull_Generic()
{
// Arrange
var value = (bool?)null;
var expected = (bool?)null;
// Act
var actual = BindConverter.FormatValue<bool?>(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_Int()
{
// Arrange
var value = 17;
var expected = "17";
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_Int_Generic()
{
// Arrange
var value = 17;
var expected = "17";
// Act
var actual = BindConverter.FormatValue<int>(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_NullableInt()
{
// Arrange
var value = (int?)17;
var expected = "17";
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_NullableInt_Generic()
{
// Arrange
var value = 17;
var expected = "17";
// Act
var actual = BindConverter.FormatValue<int?>(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_DateTime()
{
// Arrange
var value = DateTime.Now;
var expected = value.ToString(CultureInfo.CurrentCulture);
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_DateTime_Format()
{
// Arrange
var value = DateTime.Now;
var expected = value.ToString("MM-yyyy", CultureInfo.InvariantCulture);
// Act
var actual = BindConverter.FormatValue(value, "MM-yyyy", CultureInfo.InvariantCulture);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_Enum()
{
// Arrange
var value = SomeLetters.A;
var expected = value.ToString();
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_Enum_OutOfRange()
{
// Arrange
var value = SomeLetters.A + 3;
var expected = value.ToString();
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void FormatValue_NullableEnum()
{
// Arrange
var value = (SomeLetters?)null;
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Null(actual);
}
[Fact]
public void FormatValue_TypeConverter()
{
// Arrange
var value = new Person()
{
Name = "Glenn",
Age = 47,
};
var expected = JsonSerializer.Serialize(value);
// Act
var actual = BindConverter.FormatValue(value);
// Assert
Assert.Equal(expected, actual);
}
private enum SomeLetters
{
A,
B,
C,
Q,
}
[TypeConverter(typeof(PersonConverter))]
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
private class PersonConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string text)
{
return JsonSerializer.Deserialize<Person>(text);
}
return base.ConvertFrom(context, culture, value);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
{
return true;
}
return base.CanConvertTo(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
return JsonSerializer.Serialize((Person)value);
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
}
}