diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 9405897b79..f7a8157984 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -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 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(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 { diff --git a/src/Components/Components/src/BindConverter.cs b/src/Components/Components/src/BindConverter.cs new file mode 100644 index 0000000000..f77689bde2 --- /dev/null +++ b/src/Components/Components/src/BindConverter.cs @@ -0,0 +1,1418 @@ +// 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.Collections.Concurrent; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Performs conversions during binding. + /// + // + // Perf: our conversion routines present a regular API surface that allows us to specialize on types to avoid boxing. + // for instance, many of these types could be cast to IFormattable to do the appropriate formatting, but that's going + // to allocate. + public static class BindConverter + { + private static object BoxedTrue = true; + private static object BoxedFalse = false; + + private delegate object BindFormatter(T value, CultureInfo culture); + private delegate object BindFormatterWithFormat(T value, CultureInfo culture, string format); + + internal delegate bool BindParser(object obj, CultureInfo culture, out T value); + internal delegate bool BindParserWithFormat(object obj, CultureInfo culture, string format, out T value); + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(string value, CultureInfo culture = null) => FormatStringValueCore(value, culture); + + private static string FormatStringValueCore(string value, CultureInfo culture) + { + return value; + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static bool FormatValue(bool value, CultureInfo culture = null) + { + // Formatting for bool is special-cased. We need to produce a boolean value for conditional attributes + // to work. + return value; + } + + // Used with generics + private static object FormatBoolValueCore(bool value, CultureInfo culture) + { + // Formatting for bool is special-cased. We need to produce a boolean value for conditional attributes + // to work. + return value ? BoxedTrue : BoxedFalse; + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static bool? FormatValue(bool? value, CultureInfo culture = null) + { + // Formatting for bool is special-cased. We need to produce a boolean value for conditional attributes + // to work. + return value == null ? (bool?)null : value.Value; + } + + // Used with generics + private static object FormatNullableBoolValueCore(bool? value, CultureInfo culture) + { + // Formatting for bool is special-cased. We need to produce a boolean value for conditional attributes + // to work. + return value == null ? null : value.Value ? BoxedTrue : BoxedFalse; + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(int value, CultureInfo culture = null) => FormatIntValueCore(value, culture); + + private static string FormatIntValueCore(int value, CultureInfo culture) + { + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(int? value, CultureInfo culture = null) => FormatNullableIntValueCore(value, culture); + + private static string FormatNullableIntValueCore(int? value, CultureInfo culture) + { + if (value == null) + { + return null; + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(long value, CultureInfo culture = null) => FormatLongValueCore(value, culture); + + private static string FormatLongValueCore(long value, CultureInfo culture) + { + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(long? value, CultureInfo culture = null) => FormatNullableLongValueCore(value, culture); + + private static string FormatNullableLongValueCore(long? value, CultureInfo culture) + { + if (value == null) + { + return null; + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(float value, CultureInfo culture = null) => FormatFloatValueCore(value, culture); + + private static string FormatFloatValueCore(float value, CultureInfo culture) + { + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(float? value, CultureInfo culture = null) => FormatNullableFloatValueCore(value, culture); + + private static string FormatNullableFloatValueCore(float? value, CultureInfo culture) + { + if (value == null) + { + return null; + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(double value, CultureInfo culture = null) => FormatDoubleValueCore(value, culture); + + private static string FormatDoubleValueCore(double value, CultureInfo culture) + { + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(double? value, CultureInfo culture = null) => FormatNullableDoubleValueCore(value, culture); + + private static string FormatNullableDoubleValueCore(double? value, CultureInfo culture) + { + if (value == null) + { + return null; + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(decimal value, CultureInfo culture = null) => FormatDecimalValueCore(value, culture); + + private static string FormatDecimalValueCore(decimal value, CultureInfo culture) + { + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(decimal? value, CultureInfo culture = null) => FormatNullableDecimalValueCore(value, culture); + + private static string FormatNullableDecimalValueCore(decimal? value, CultureInfo culture) + { + if (value == null) + { + return null; + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(DateTime value, CultureInfo culture = null) => FormatDateTimeValueCore(value, format: null, culture); + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// The format to use. Provided to . + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(DateTime value, string format, CultureInfo culture = null) => FormatDateTimeValueCore(value, format, culture); + + private static string FormatDateTimeValueCore(DateTime value, string format, CultureInfo culture) + { + if (format != null) + { + return value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + private static string FormatDateTimeValueCore(DateTime value, CultureInfo culture) + { + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(DateTime? value, CultureInfo culture = null) => FormatNullableDateTimeValueCore(value, format: null, culture); + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// The format to use. Provided to . + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(DateTime? value, string format, CultureInfo culture = null) => FormatNullableDateTimeValueCore(value, format, culture); + + private static string FormatNullableDateTimeValueCore(DateTime? value, string format, CultureInfo culture) + { + if (value == null) + { + return null; + } + + if (format != null) + { + return value.Value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + private static string FormatNullableDateTimeValueCore(DateTime? value, CultureInfo culture) + { + if (value == null) + { + return null; + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(DateTimeOffset value, CultureInfo culture = null) => FormatDateTimeOffsetValueCore(value, format: null, culture); + + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// The format to use. Provided to . + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(DateTimeOffset value, string format, CultureInfo culture = null) => FormatDateTimeOffsetValueCore(value, format, culture); + + private static string FormatDateTimeOffsetValueCore(DateTimeOffset value, string format, CultureInfo culture) + { + if (format != null) + { + return value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + private static string FormatDateTimeOffsetValueCore(DateTimeOffset value, CultureInfo culture) + { + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(DateTimeOffset? value, CultureInfo culture = null) => FormatNullableDateTimeOffsetValueCore(value, format: null, culture); + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// The format to use. Provided to . + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static string FormatValue(DateTimeOffset? value, string format, CultureInfo culture = null) => FormatNullableDateTimeOffsetValueCore(value, format, culture); + + private static string FormatNullableDateTimeOffsetValueCore(DateTimeOffset? value, string format, CultureInfo culture) + { + if (value == null) + { + return null; + } + + if (format != null) + { + return value.Value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + private static string FormatNullableDateTimeOffsetValueCore(DateTimeOffset? value, CultureInfo culture) + { + if (value == null) + { + return null; + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + private static string FormatEnumValueCore(T value, CultureInfo culture) where T : struct, Enum + { + return value.ToString(); // The overload that acccepts a culture is [Obsolete] + } + + private static string FormatNullableEnumValueCore(T? value, CultureInfo culture) where T : struct, Enum + { + if (value == null) + { + return null; + } + + return value.Value.ToString(); // The overload that acccepts a culture is [Obsolete] + } + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + public static object FormatValue(T value, CultureInfo culture = null) + { + var formatter = FormatterDelegateCache.Get(); + return formatter(value, culture); + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToString(object obj, CultureInfo culture, out string value) + { + return ConvertToStringCore(obj, culture, out value); + } + + internal readonly static BindParser 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; + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToBool(object obj, CultureInfo culture, out bool value) + { + return ConvertToBoolCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableBool(object obj, CultureInfo culture, out bool? value) + { + return ConvertToNullableBoolCore(obj, culture, out value); + } + + internal static BindParser ConvertToBool = ConvertToBoolCore; + internal static BindParser 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; + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToInt(object obj, CultureInfo culture, out int value) + { + return ConvertToIntCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableInt(object obj, CultureInfo culture, out int? value) + { + return ConvertToNullableIntCore(obj, culture, out value); + } + + internal static BindParser ConvertToInt = ConvertToIntCore; + internal static BindParser 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; + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToLong(object obj, CultureInfo culture, out long value) + { + return ConvertToLongCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableLong(object obj, CultureInfo culture, out long? value) + { + return ConvertToNullableLongCore(obj, culture, out value); + } + + internal static BindParser ConvertToLong = ConvertToLongCore; + internal static BindParser 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; + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToFloat(object obj, CultureInfo culture, out float value) + { + return ConvertToFloatCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableFloat(object obj, CultureInfo culture, out float? value) + { + return ConvertToNullableFloatCore(obj, culture, out value); + } + + internal static BindParser ConvertToFloat = ConvertToFloatCore; + internal static BindParser 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; + } + + if (float.IsInfinity(converted) || float.IsNaN(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; + } + + if (float.IsInfinity(converted) || float.IsNaN(converted)) + { + value = default; + return false; + } + + value = converted; + return true; + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToDouble(object obj, CultureInfo culture, out double value) + { + return ConvertToDoubleCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableDouble(object obj, CultureInfo culture, out double? value) + { + return ConvertToNullableDoubleCore(obj, culture, out value); + } + + internal static BindParser ConvertToDoubleDelegate = ConvertToDoubleCore; + internal static BindParser ConvertToNullableDoubleDelegate = 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; + } + + if (double.IsInfinity(converted) || double.IsNaN(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; + } + + if (double.IsInfinity(converted) || double.IsNaN(converted)) + { + value = default; + return false; + } + + value = converted; + return true; + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToDecimal(object obj, CultureInfo culture, out decimal value) + { + return ConvertToDecimalCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableDecimal(object obj, CultureInfo culture, out decimal? value) + { + return ConvertToNullableDecimalCore(obj, culture, out value); + } + + internal static BindParser ConvertToDecimal = ConvertToDecimalCore; + internal static BindParser 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; + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToDateTime(object obj, CultureInfo culture, out DateTime value) + { + return ConvertToDateTimeCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The format string to use in conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToDateTime(object obj, CultureInfo culture, string format, out DateTime value) + { + return ConvertToDateTimeCore(obj, culture, format, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableDateTime(object obj, CultureInfo culture, out DateTime? value) + { + return ConvertToNullableDateTimeCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The format string to use in conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableDateTime(object obj, CultureInfo culture, string format, out DateTime? value) + { + return ConvertToNullableDateTimeCore(obj, culture, format, out value); + } + + internal static BindParser ConvertToDateTime = ConvertToDateTimeCore; + internal static BindParserWithFormat ConvertToDateTimeWithFormat = ConvertToDateTimeCore; + internal static BindParser ConvertToNullableDateTime = ConvertToNullableDateTimeCore; + internal static BindParserWithFormat 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; + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToDateTimeOffset(object obj, CultureInfo culture, out DateTimeOffset value) + { + return ConvertToDateTimeOffsetCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a . + /// + /// The object to convert. + /// The to use for conversion. + /// The format string to use in conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToDateTimeOffset(object obj, CultureInfo culture, string format, out DateTimeOffset value) + { + return ConvertToDateTimeOffsetCore(obj, culture, format, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableDateTimeOffset(object obj, CultureInfo culture, out DateTimeOffset? value) + { + return ConvertToNullableDateTimeOffsetCore(obj, culture, out value); + } + + /// + /// Attempts to convert a value to a nullable . + /// + /// The object to convert. + /// The to use for conversion. + /// The format string to use in conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertToNullableDateTimeOffset(object obj, CultureInfo culture, string format, out DateTimeOffset? value) + { + return ConvertToNullableDateTimeOffsetCore(obj, culture, format, out value); + } + + internal static BindParser ConvertToDateTimeOffset = ConvertToDateTimeOffsetCore; + internal static BindParserWithFormat ConvertToDateTimeOffsetWithFormat = ConvertToDateTimeOffsetCore; + internal static BindParser ConvertToNullableDateTimeOffset = ConvertToNullableDateTimeOffsetCore; + internal static BindParserWithFormat 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(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(text, out var converted)) + { + value = default; + return false; + } + + if (!Enum.IsDefined(typeof(T), converted)) + { + value = default; + return false; + } + + value = converted; + return true; + } + + private static bool ConvertToNullableEnum(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(text, out var converted)) + { + value = default; + return false; + } + + if (!Enum.IsDefined(typeof(T), converted)) + { + value = default; + return false; + } + + value = converted; + return true; + } + + /// + /// Attempts to convert a value to a value of type . + /// + /// The object to convert. + /// The to use for conversion. + /// The converted value. + /// true if conversion is successful, otherwise false. + public static bool TryConvertTo(object obj, CultureInfo culture, out T value) + { + var converter = ParserDelegateCache.Get(); + return converter(obj, culture, out value); + } + + private static class FormatterDelegateCache + { + private readonly static ConcurrentDictionary _cache = new ConcurrentDictionary(); + + private static MethodInfo _formatEnumValue; + private static MethodInfo _formatNullableEnumValue; + + public static BindFormatter Get() + { + if (!_cache.TryGetValue(typeof(T), out var formattter)) + { + // 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)) + { + formattter = (BindFormatter)FormatStringValueCore; + } + else if (typeof(T) == typeof(bool)) + { + formattter = (BindFormatter)FormatBoolValueCore; + } + else if (typeof(T) == typeof(bool?)) + { + formattter = (BindFormatter)FormatNullableBoolValueCore; + } + else if (typeof(T) == typeof(int)) + { + formattter = (BindFormatter)FormatIntValueCore; + } + else if (typeof(T) == typeof(int?)) + { + formattter = (BindFormatter)FormatNullableIntValueCore; + } + else if (typeof(T) == typeof(long)) + { + formattter = (BindFormatter)FormatLongValueCore; + } + else if (typeof(T) == typeof(long?)) + { + formattter = (BindFormatter)FormatNullableLongValueCore; + } + else if (typeof(T) == typeof(float)) + { + formattter = (BindFormatter)FormatFloatValueCore; + } + else if (typeof(T) == typeof(float?)) + { + formattter = (BindFormatter)FormatNullableFloatValueCore; + } + else if (typeof(T) == typeof(double)) + { + formattter = (BindFormatter)FormatDoubleValueCore; + } + else if (typeof(T) == typeof(double?)) + { + formattter = (BindFormatter)FormatNullableDoubleValueCore; + } + else if (typeof(T) == typeof(decimal)) + { + formattter = (BindFormatter)FormatDecimalValueCore; + } + else if (typeof(T) == typeof(decimal?)) + { + formattter = (BindFormatter)FormatNullableDecimalValueCore; + } + else if (typeof(T) == typeof(DateTime)) + { + formattter = (BindFormatter)FormatDateTimeValueCore; + } + else if (typeof(T) == typeof(DateTime?)) + { + formattter = (BindFormatter)FormatNullableDateTimeValueCore; + } + else if (typeof(T) == typeof(DateTimeOffset)) + { + formattter = (BindFormatter)FormatDateTimeOffsetValueCore; + } + else if (typeof(T) == typeof(DateTimeOffset?)) + { + formattter = (BindFormatter)FormatNullableDateTimeOffsetValueCore; + } + else if (typeof(T).IsEnum) + { + // We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse. + var method = _formatEnumValue ??= typeof(BindConverter).GetMethod(nameof(FormatEnumValueCore), BindingFlags.NonPublic | BindingFlags.Static); + formattter = method.MakeGenericMethod(typeof(T)).CreateDelegate(typeof(BindFormatter), 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 = _formatNullableEnumValue ??= typeof(BindConverter).GetMethod(nameof(FormatNullableEnumValueCore), BindingFlags.NonPublic | BindingFlags.Static); + formattter = method.MakeGenericMethod(innerType).CreateDelegate(typeof(BindFormatter), target: null); + } + else + { + formattter = MakeTypeConverterFormatter(); + } + + _cache.TryAdd(typeof(T), formattter); + } + + return (BindFormatter)formattter; + } + + private static BindFormatter MakeTypeConverterFormatter() + { + var typeConverter = TypeDescriptor.GetConverter(typeof(T)); + if (typeConverter == null || !typeConverter.CanConvertTo(typeof(string))) + { + throw new InvalidOperationException( + $"The type '{typeof(T).FullName}' does not have an associated {typeof(TypeConverter).Name} that supports " + + $"conversion to a string. " + + $"Apply '{typeof(TypeConverterAttribute).Name}' to the type to register a converter."); + } + + return FormatWithTypeConverter; + + string FormatWithTypeConverter(T value, CultureInfo culture) + { + // We intentionally close-over the TypeConverter to cache it. The TypeDescriptor infrastructure is slow. + return typeConverter.ConvertToString(context: null, culture ?? CultureInfo.CurrentCulture, value); + } + } + } + + internal static class ParserDelegateCache + { + private readonly static ConcurrentDictionary _cache = new ConcurrentDictionary(); + + private static MethodInfo _convertToEnum; + private static MethodInfo _convertToNullableEnum; + + public static BindParser Get() + { + if (!_cache.TryGetValue(typeof(T), out var parser)) + { + // 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)) + { + parser = ConvertToString; + } + else if (typeof(T) == typeof(bool)) + { + parser = ConvertToBool; + } + else if (typeof(T) == typeof(bool?)) + { + parser = ConvertToNullableBool; + } + else if (typeof(T) == typeof(int)) + { + parser = ConvertToInt; + } + else if (typeof(T) == typeof(int?)) + { + parser = ConvertToNullableInt; + } + else if (typeof(T) == typeof(long)) + { + parser = ConvertToLong; + } + else if (typeof(T) == typeof(long?)) + { + parser = ConvertToNullableLong; + } + else if (typeof(T) == typeof(float)) + { + parser = ConvertToFloat; + } + else if (typeof(T) == typeof(float?)) + { + parser = ConvertToNullableFloat; + } + else if (typeof(T) == typeof(double)) + { + parser = ConvertToDoubleDelegate; + } + else if (typeof(T) == typeof(double?)) + { + parser = ConvertToNullableDoubleDelegate; + } + else if (typeof(T) == typeof(decimal)) + { + parser = ConvertToDecimal; + } + else if (typeof(T) == typeof(decimal?)) + { + parser = ConvertToNullableDecimal; + } + else if (typeof(T) == typeof(DateTime)) + { + parser = ConvertToDateTime; + } + else if (typeof(T) == typeof(DateTime?)) + { + parser = ConvertToNullableDateTime; + } + else if (typeof(T) == typeof(DateTimeOffset)) + { + parser = ConvertToDateTime; + } + else if (typeof(T) == typeof(DateTimeOffset?)) + { + parser = ConvertToNullableDateTime; + } + 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(BindConverter).GetMethod(nameof(ConvertToEnum), BindingFlags.NonPublic | BindingFlags.Static); + parser = method.MakeGenericMethod(typeof(T)).CreateDelegate(typeof(BindParser), 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(BindConverter).GetMethod(nameof(ConvertToNullableEnum), BindingFlags.NonPublic | BindingFlags.Static); + parser = method.MakeGenericMethod(innerType).CreateDelegate(typeof(BindParser), target: null); + } + else + { + parser = MakeTypeConverterConverter(); + } + + _cache.TryAdd(typeof(T), parser); + } + + return (BindParser)parser; + } + + private static BindParser MakeTypeConverterConverter() + { + 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) + { + // We intentionally close-over the TypeConverter to cache it. The TypeDescriptor infrastructure is slow. + var converted = typeConverter.ConvertFrom(context: null, culture ?? CultureInfo.CurrentCulture, obj); + if (converted == null) + { + value = default; + return true; + } + + value = (T)converted; + return true; + } + } + } + } +} diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs index 0f8a6d547e..a007c47f09 100644 --- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs +++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs @@ -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(object obj, CultureInfo culture, out T value); - private delegate bool BindConverterWithFormat(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 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 ConvertToBool = ConvertToBoolCore; - private static BindConverter 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 ConvertToInt = ConvertToIntCore; - private static BindConverter 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 ConvertToLong = ConvertToLongCore; - private static BindConverter 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 ConvertToFloat = ConvertToFloatCore; - private static BindConverter 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 ConvertToDouble = ConvertToDoubleCore; - private static BindConverter 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 ConvertToDecimal = ConvertToDecimalCore; - private static BindConverter 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 ConvertToDateTime = ConvertToDateTimeCore; - private static BindConverterWithFormat ConvertToDateTimeWithFormat = ConvertToDateTimeCore; - private static BindConverter ConvertToNullableDateTime = ConvertToNullableDateTimeCore; - private static BindConverterWithFormat 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 ConvertToDateTimeOffset = ConvertToDateTimeOffsetCore; - private static BindConverterWithFormat ConvertToDateTimeOffsetWithFormat = ConvertToDateTimeOffsetCore; - private static BindConverter ConvertToNullableDateTimeOffset = ConvertToNullableDateTimeOffsetCore; - private static BindConverterWithFormat 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(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(text, out var converted)) - { - value = default; - return false; - } - - value = converted; - return true; - } - - private static bool ConvertToNullableEnum(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(text, out var converted)) - { - value = default; - return false; - } - - value = converted; - return true; - } - /// /// For internal use only. /// @@ -611,7 +209,7 @@ namespace Microsoft.AspNetCore.Components double existingValue, CultureInfo culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToDouble); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDoubleDelegate); } /// @@ -630,7 +228,7 @@ namespace Microsoft.AspNetCore.Components double? existingValue, CultureInfo culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDouble); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDoubleDelegate); } /// @@ -687,7 +285,7 @@ namespace Microsoft.AspNetCore.Components DateTime existingValue, CultureInfo culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format: null, ConvertToDateTimeWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateTime); } /// @@ -727,7 +325,7 @@ namespace Microsoft.AspNetCore.Components DateTime? existingValue, CultureInfo culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format: null, ConvertToNullableDateTimeWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateTime); } /// @@ -767,7 +365,7 @@ namespace Microsoft.AspNetCore.Components DateTimeOffset existingValue, CultureInfo culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format: null, ConvertToDateTimeOffsetWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateTimeOffset); } /// @@ -807,7 +405,7 @@ namespace Microsoft.AspNetCore.Components DateTimeOffset? existingValue, CultureInfo culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format: null, ConvertToNullableDateTimeOffsetWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateTimeOffset); } /// @@ -848,7 +446,7 @@ namespace Microsoft.AspNetCore.Components T existingValue, CultureInfo culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, BinderConverterCache.Get()); + return CreateBinderCore(factory, receiver, setter, culture, ParserDelegateCache.Get()); } private static EventCallback CreateBinderCore( @@ -856,7 +454,7 @@ namespace Microsoft.AspNetCore.Components object receiver, Action setter, CultureInfo culture, - BindConverter converter) + BindConverter.BindParser converter) { Action callback = e => { @@ -900,7 +498,7 @@ namespace Microsoft.AspNetCore.Components Action setter, CultureInfo culture, string format, - BindConverterWithFormat converter) + BindConverter.BindParserWithFormat converter) { Action callback = e => { @@ -937,147 +535,5 @@ namespace Microsoft.AspNetCore.Components }; return factory.Create(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 _cache = new ConcurrentDictionary(); - - private static MethodInfo _convertToEnum; - private static MethodInfo _convertToNullableEnum; - - public static BindConverter Get() - { - 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), 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), target: null); - } - else - { - converter = MakeTypeConverterConverter(); - } - - _cache.TryAdd(typeof(T), converter); - } - - return (BindConverter)converter; - } - - private static BindConverter MakeTypeConverterConverter() - { - 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; - } - } - } } } diff --git a/src/Components/Components/src/Forms/InputComponents/InputCheckbox.cs b/src/Components/Components/src/Forms/InputComponents/InputCheckbox.cs index df84c8bdaa..981287ee8d 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputCheckbox.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputCheckbox.cs @@ -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(this, __value => CurrentValue = __value, CurrentValue)); builder.CloseElement(); } diff --git a/src/Components/Components/src/Forms/InputComponents/InputDate.cs b/src/Components/Components/src/Forms/InputComponents/InputDate.cs index 0a79bbc15d..f080af908d 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputDate.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputDate.cs @@ -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(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, 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; diff --git a/src/Components/Components/src/Forms/InputComponents/InputNumber.cs b/src/Components/Components/src/Forms/InputComponents/InputNumber.cs index 044f4aac3d..c5a222f6ff 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputNumber.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputNumber.cs @@ -13,38 +13,18 @@ namespace Microsoft.AspNetCore.Components.Forms /// public class InputNumber : InputBase { - 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, 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(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); builder.CloseElement(); } @@ -74,7 +54,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) { - if (_parser(value, out result)) + if (BindConverter.TryConvertTo(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; - } - } } } diff --git a/src/Components/Components/src/Forms/InputComponents/InputSelect.cs b/src/Components/Components/src/Forms/InputComponents/InputSelect.cs index 6705ce9320..fda1822afe 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputSelect.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputSelect.cs @@ -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(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(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."; diff --git a/src/Components/Components/src/Forms/InputComponents/InputText.cs b/src/Components/Components/src/Forms/InputComponents/InputText.cs index ffb7004601..94c09c4694 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputText.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputText.cs @@ -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(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); builder.CloseElement(); } diff --git a/src/Components/Components/src/Forms/InputComponents/InputTextArea.cs b/src/Components/Components/src/Forms/InputComponents/InputTextArea.cs index ed676d6404..ba61d95896 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputTextArea.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputTextArea.cs @@ -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(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); builder.CloseElement(); } diff --git a/src/Components/Components/test/BindConverterTest.cs b/src/Components/Components/test/BindConverterTest.cs new file mode 100644 index 0000000000..c6fe1275cc --- /dev/null +++ b/src/Components/Components/test/BindConverterTest.cs @@ -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(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(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(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(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(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(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); + } + } + } +}