From c5cf99f6a24d9da3468989fefdff9036aac5bd4e Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 4 Jul 2019 17:31:19 -0700 Subject: [PATCH] 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). --- ...ft.AspNetCore.Components.netstandard2.0.cs | 47 + .../Components/src/BindConverter.cs | 1418 +++++++++++++++++ .../EventCallbackFactoryBinderExtensions.cs | 564 +------ .../Forms/InputComponents/InputCheckbox.cs | 2 +- .../src/Forms/InputComponents/InputDate.cs | 10 +- .../src/Forms/InputComponents/InputNumber.cs | 119 +- .../src/Forms/InputComponents/InputSelect.cs | 11 +- .../src/Forms/InputComponents/InputText.cs | 2 +- .../Forms/InputComponents/InputTextArea.cs | 2 +- .../Components/test/BindConverterTest.cs | 307 ++++ 10 files changed, 1808 insertions(+), 674 deletions(-) create mode 100644 src/Components/Components/src/BindConverter.cs create mode 100644 src/Components/Components/test/BindConverterTest.cs 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); + } + } + } +}