Add support for TypeConverter (#10730)
* Add support for TypeConverter Fixes: #8493 Fixes: #9632 Fixes: #9339 Fixes: #8385 Fixes: 10077 This fix adds support for type converters as well as a few other minor things we were missing from binding. The key thing about supporting conversions is that we new can support arbitrary types with `@bind`. This means you can use it with generics, which is something many users have tried. Along with type converters we get Guid and TimeSpan from the BCL. The BCL also includes converters for types we're less interested in like `short`. * Use correct NumberStyles * Fix culture * Core check
This commit is contained in:
parent
03d94e0ee6
commit
5af8e170bc
|
|
@ -13,5 +13,9 @@
|
|||
<assembly fullname="System">
|
||||
<!-- Without this, [Required(typeof(bool), "true", "true", ErrorMessage = "...")] fails -->
|
||||
<type fullname="System.ComponentModel.BooleanConverter" />
|
||||
|
||||
<!-- TypeConverters are only used through reflection. These are two built-in TypeConverters that are useful. -->
|
||||
<type fullname="System.ComponentModel.GuidConverter" />
|
||||
<type fullname="System.ComponentModel.TimeSpanConverter" />
|
||||
</assembly>
|
||||
</linker>
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<int> setter, int existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<long> setter, long existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<bool?> setter, bool? existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<System.DateTime?> setter, System.DateTime? existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<decimal?> setter, decimal? existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<double?> setter, double? existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<int?> setter, int? existingValue) { throw null; }
|
||||
|
|
@ -162,7 +163,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<float?> setter, float? existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<float> setter, float existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<string> setter, string existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder<T>(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<T> setter, T existingValue) where T : struct, System.Enum { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder<T>(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<T> setter, T existingValue) { throw null; }
|
||||
}
|
||||
public static partial class EventCallbackFactoryUIEventArgsExtensions
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,13 +2,27 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for two-way binding using <see cref="EventCallback"/>. For internal use only.
|
||||
/// </summary>
|
||||
//
|
||||
// NOTE: for number parsing, the HTML5 spec dictates that <input type="number"> the DOM will represent
|
||||
// number values as floating point numbers using `.` as the period separator. This is NOT culture senstive.
|
||||
// Put another way, the user might see `,` as their decimal separator, but the value available in events
|
||||
// to JS code is always simpilar to what .NET parses with InvariantCulture.
|
||||
//
|
||||
// See: https://www.w3.org/TR/html5/sec-forms.html#number-state-typenumber
|
||||
// See: https://www.w3.org/TR/html5/infrastructure.html#valid-floating-point-number
|
||||
//
|
||||
// For now we're not necessarily handling this correctly since we parse the same way for number and text.
|
||||
public static class EventCallbackFactoryBinderExtensions
|
||||
{
|
||||
private delegate bool BindConverter<T>(object obj, out T value);
|
||||
|
|
@ -53,7 +67,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(text, out var converted))
|
||||
if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -72,7 +86,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!int.TryParse(text, out var converted))
|
||||
if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -94,7 +108,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!long.TryParse(text, out var converted))
|
||||
if (!long.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -113,7 +127,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!long.TryParse(text, out var converted))
|
||||
if (!long.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -135,7 +149,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!float.TryParse(text, out var converted))
|
||||
if (!float.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -154,7 +168,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!float.TryParse(text, out var converted))
|
||||
if (!float.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -176,7 +190,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!double.TryParse(text, out var converted))
|
||||
if (!double.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -195,7 +209,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!double.TryParse(text, out var converted))
|
||||
if (!double.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -217,7 +231,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!decimal.TryParse(text, out var converted))
|
||||
if (!decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -236,7 +250,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!decimal.TryParse(text, out var converted))
|
||||
if (!decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
|
|
@ -246,28 +260,83 @@ namespace Microsoft.AspNetCore.Components
|
|||
return true;
|
||||
}
|
||||
|
||||
private static class EnumConverter<T> where T : struct, Enum
|
||||
private static BindConverter<DateTime> ConvertToDateTime = ConvertToDateTimeCore;
|
||||
private static BindConverter<DateTime?> ConvertToNullableDateTime = ConvertToNullableDateTimeCore;
|
||||
|
||||
private static bool ConvertToDateTimeCore(object obj, out DateTime value)
|
||||
{
|
||||
public static readonly BindConverter<T> Convert = ConvertCore;
|
||||
|
||||
public static bool ConvertCore(object obj, out T value)
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<T>(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
if (!DateTime.TryParse(text, CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertToNullableDateTimeCore(object obj, out DateTime? value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!DateTime.TryParse(text, CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertToEnum<T>(object obj, out T value) where T : struct, Enum
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<T>(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertToNullableEnum<T>(object obj, out Nullable<T> value) where T : struct, Enum
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<T>(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -284,7 +353,6 @@ namespace Microsoft.AspNetCore.Components
|
|||
Action<string> setter,
|
||||
string existingValue)
|
||||
{
|
||||
;
|
||||
return CreateBinderCore<string>(factory, receiver, setter, ConvertToString);
|
||||
}
|
||||
|
||||
|
|
@ -489,15 +557,6 @@ namespace Microsoft.AspNetCore.Components
|
|||
Action<decimal?> setter,
|
||||
decimal? existingValue)
|
||||
{
|
||||
Func<object, decimal?> converter = (obj) =>
|
||||
{
|
||||
if (decimal.TryParse((string)obj, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
return CreateBinderCore<decimal?>(factory, receiver, setter, ConvertToNullableDecimal);
|
||||
}
|
||||
|
||||
|
|
@ -515,28 +574,24 @@ namespace Microsoft.AspNetCore.Components
|
|||
Action<DateTime> setter,
|
||||
DateTime existingValue)
|
||||
{
|
||||
// Avoiding CreateBinderCore so we can avoid an extra allocating lambda
|
||||
// when a format is used.
|
||||
Action<UIChangeEventArgs> callback = (e) =>
|
||||
{
|
||||
DateTime value = default;
|
||||
var converted = false;
|
||||
try
|
||||
{
|
||||
value = ConvertDateTime(e.Value, format: null);
|
||||
converted = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return CreateBinderCore<DateTime>(factory, receiver, setter, ConvertToDateTime);
|
||||
}
|
||||
|
||||
// See comments in CreateBinderCore
|
||||
if (converted)
|
||||
{
|
||||
setter(value);
|
||||
}
|
||||
};
|
||||
return factory.Create<UIChangeEventArgs>(receiver, callback);
|
||||
/// <summary>
|
||||
/// For internal use only.
|
||||
/// </summary>
|
||||
/// <param name="factory"></param>
|
||||
/// <param name="receiver"></param>
|
||||
/// <param name="setter"></param>
|
||||
/// <param name="existingValue"></param>
|
||||
/// <returns></returns>
|
||||
public static EventCallback<UIChangeEventArgs> CreateBinder(
|
||||
this EventCallbackFactory factory,
|
||||
object receiver,
|
||||
Action<DateTime?> setter,
|
||||
DateTime? existingValue)
|
||||
{
|
||||
return CreateBinderCore<DateTime?>(factory, receiver, setter, ConvertToNullableDateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -577,6 +632,23 @@ namespace Microsoft.AspNetCore.Components
|
|||
}
|
||||
};
|
||||
return factory.Create<UIChangeEventArgs>(receiver, callback);
|
||||
|
||||
static DateTime ConvertDateTime(object obj, string format)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
else if (format != null && DateTime.TryParseExact(text, format, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DateTime.Parse(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -592,26 +664,9 @@ namespace Microsoft.AspNetCore.Components
|
|||
this EventCallbackFactory factory,
|
||||
object receiver,
|
||||
Action<T> setter,
|
||||
T existingValue) where T : struct, Enum
|
||||
T existingValue)
|
||||
{
|
||||
return CreateBinderCore<T>(factory, receiver, setter, EnumConverter<T>.Convert);
|
||||
}
|
||||
|
||||
private static DateTime ConvertDateTime(object obj, string format)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
else if (format != null && DateTime.TryParseExact(text, format, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DateTime.Parse(text);
|
||||
}
|
||||
return CreateBinderCore<T>(factory, receiver, setter, BinderConverterCache.Get<T>());
|
||||
}
|
||||
|
||||
private static EventCallback<UIChangeEventArgs> CreateBinderCore<T>(
|
||||
|
|
@ -642,5 +697,139 @@ namespace Microsoft.AspNetCore.Components
|
|||
};
|
||||
return factory.Create<UIChangeEventArgs>(receiver, callback);
|
||||
}
|
||||
|
||||
// We can't rely on generics + static to cache here unfortunately. That would require us to overload
|
||||
// CreateBinder on T : struct AND T : class, which is not allowed.
|
||||
private static class BinderConverterCache
|
||||
{
|
||||
private readonly static ConcurrentDictionary<Type, Delegate> _cache = new ConcurrentDictionary<Type, Delegate>();
|
||||
|
||||
private static MethodInfo _convertToEnum;
|
||||
private static MethodInfo _convertToNullableEnum;
|
||||
|
||||
public static BindConverter<T> Get<T>()
|
||||
{
|
||||
if (!_cache.TryGetValue(typeof(T), out var converter))
|
||||
{
|
||||
// We need to replicate all of the primitive cases that we handle here so that they will behave the same way.
|
||||
// The result will be cached.
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
converter = ConvertToString;
|
||||
}
|
||||
else if (typeof(T) == typeof(bool))
|
||||
{
|
||||
converter = ConvertToBool;
|
||||
}
|
||||
else if (typeof(T) == typeof(bool?))
|
||||
{
|
||||
converter = ConvertToNullableBool;
|
||||
}
|
||||
else if (typeof(T) == typeof(int))
|
||||
{
|
||||
converter = ConvertToInt;
|
||||
}
|
||||
else if (typeof(T) == typeof(int?))
|
||||
{
|
||||
converter = ConvertToNullableInt;
|
||||
}
|
||||
else if (typeof(T) == typeof(long))
|
||||
{
|
||||
converter = ConvertToLong;
|
||||
}
|
||||
else if (typeof(T) == typeof(long?))
|
||||
{
|
||||
converter = ConvertToNullableLong;
|
||||
}
|
||||
else if (typeof(T) == typeof(float))
|
||||
{
|
||||
converter = ConvertToFloat;
|
||||
}
|
||||
else if (typeof(T) == typeof(float?))
|
||||
{
|
||||
converter = ConvertToNullableFloat;
|
||||
}
|
||||
else if (typeof(T) == typeof(double))
|
||||
{
|
||||
converter = ConvertToDouble;
|
||||
}
|
||||
else if (typeof(T) == typeof(double?))
|
||||
{
|
||||
converter = ConvertToNullableDouble;
|
||||
}
|
||||
else if (typeof(T) == typeof(decimal))
|
||||
{
|
||||
converter = ConvertToDecimal;
|
||||
}
|
||||
else if (typeof(T) == typeof(decimal?))
|
||||
{
|
||||
converter = ConvertToNullableDecimal;
|
||||
}
|
||||
else if (typeof(T) == typeof(DateTime))
|
||||
{
|
||||
converter = ConvertToDateTime;
|
||||
}
|
||||
else if (typeof(T) == typeof(DateTime?))
|
||||
{
|
||||
converter = ConvertToNullableDateTime;
|
||||
}
|
||||
else if (typeof(T).IsEnum)
|
||||
{
|
||||
// We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse.
|
||||
var method = _convertToEnum ??= typeof(EventCallbackFactoryBinderExtensions).GetMethod(nameof(ConvertToEnum), BindingFlags.NonPublic | BindingFlags.Static);
|
||||
converter = method.MakeGenericMethod(typeof(T)).CreateDelegate(typeof(BindConverter<T>), target: null);
|
||||
}
|
||||
else if (Nullable.GetUnderlyingType(typeof(T)) is Type innerType && innerType.IsEnum)
|
||||
{
|
||||
// We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse.
|
||||
var method = _convertToNullableEnum ??= typeof(EventCallbackFactoryBinderExtensions).GetMethod(nameof(ConvertToNullableEnum), BindingFlags.NonPublic | BindingFlags.Static);
|
||||
converter = method.MakeGenericMethod(innerType).CreateDelegate(typeof(BindConverter<T>), target: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
converter = MakeTypeConverterConverter<T>();
|
||||
}
|
||||
|
||||
_cache.TryAdd(typeof(T), converter);
|
||||
}
|
||||
|
||||
return (BindConverter<T>)converter;
|
||||
}
|
||||
|
||||
private static BindConverter<T> MakeTypeConverterConverter<T>()
|
||||
{
|
||||
var typeConverter = TypeDescriptor.GetConverter(typeof(T));
|
||||
if (typeConverter == null || !typeConverter.CanConvertFrom(typeof(string)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The type '{typeof(T).FullName}' does not have an associated {typeof(TypeConverter).Name} that supports " +
|
||||
$"conversion from a string. " +
|
||||
$"Apply '{typeof(TypeConverterAttribute).Name}' to the type to register a converter.");
|
||||
}
|
||||
|
||||
return ConvertWithTypeConverter;
|
||||
|
||||
bool ConvertWithTypeConverter(object obj, 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, CultureInfo.CurrentCulture, text);
|
||||
if (converted == null)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = (T)converted;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// 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.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -344,6 +346,25 @@ namespace Microsoft.AspNetCore.Components
|
|||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBinder_NullableEnum()
|
||||
{
|
||||
// Arrange
|
||||
var value = (AttributeTargets?)AttributeTargets.All;
|
||||
var component = new EventCountingComponent();
|
||||
Action<AttributeTargets?> setter = (_) => value = _;
|
||||
|
||||
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
|
||||
|
||||
var expectedValue = AttributeTargets.Class;
|
||||
|
||||
// Act
|
||||
await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue.ToString(), });
|
||||
|
||||
Assert.Equal(expectedValue, value);
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBinder_DateTime()
|
||||
{
|
||||
|
|
@ -363,6 +384,26 @@ namespace Microsoft.AspNetCore.Components
|
|||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBinder_NullableDateTime()
|
||||
{
|
||||
// Arrange
|
||||
var value = (DateTime?)DateTime.Now;
|
||||
var component = new EventCountingComponent();
|
||||
Action<DateTime?> setter = (_) => value = _;
|
||||
|
||||
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
|
||||
|
||||
var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);
|
||||
|
||||
// Act
|
||||
await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue.ToString(), });
|
||||
|
||||
Assert.Equal(expectedValue, value);
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
// For now format is only supported by this specific method.
|
||||
[Fact]
|
||||
public async Task CreateBinder_DateTime_Format()
|
||||
{
|
||||
|
|
@ -383,6 +424,80 @@ namespace Microsoft.AspNetCore.Components
|
|||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
// This uses a type converter
|
||||
[Fact]
|
||||
public async Task CreateBinder_Guid()
|
||||
{
|
||||
// Arrange
|
||||
var value = Guid.NewGuid();
|
||||
var component = new EventCountingComponent();
|
||||
Action<Guid> setter = (_) => value = _;
|
||||
|
||||
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
|
||||
|
||||
var expectedValue = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue.ToString(), });
|
||||
|
||||
Assert.Equal(expectedValue, value);
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
// This uses a type converter
|
||||
[Fact]
|
||||
public async Task CreateBinder_NullableGuid()
|
||||
{
|
||||
// Arrange
|
||||
var value = (Guid?)Guid.NewGuid();
|
||||
var component = new EventCountingComponent();
|
||||
Action<Guid?> setter = (_) => value = _;
|
||||
|
||||
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
|
||||
|
||||
var expectedValue = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue.ToString(), });
|
||||
|
||||
Assert.Equal(expectedValue, value);
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBinder_CustomTypeConverter()
|
||||
{
|
||||
// Arrange
|
||||
var value = new SecretMessage() { Message = "A message", };
|
||||
var component = new EventCountingComponent();
|
||||
Action<SecretMessage> setter = (_) => value = _;
|
||||
|
||||
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
|
||||
|
||||
var expectedValue = new SecretMessage() { Message = "TypeConverter may be old, but it still works!", };
|
||||
|
||||
// Act
|
||||
await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue.ToString(), });
|
||||
|
||||
Assert.Equal(expectedValue.Message, value.Message);
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBinder_GenericWithoutTypeConverter_Throws()
|
||||
{
|
||||
var value = new ClassWithoutTypeConverter();
|
||||
var component = new EventCountingComponent();
|
||||
Action<ClassWithoutTypeConverter> setter = (_) => value = _;
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => EventCallback.Factory.CreateBinder(component, setter, value));
|
||||
|
||||
Assert.Equal(
|
||||
$"The type '{typeof(ClassWithoutTypeConverter).FullName}' does not have an associated TypeConverter that supports conversion from a string. " +
|
||||
$"Apply 'TypeConverterAttribute' to the type to register a converter.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
private class EventCountingComponent : IComponent, IHandleEvent
|
||||
{
|
||||
public int Count;
|
||||
|
|
@ -403,5 +518,42 @@ namespace Microsoft.AspNetCore.Components
|
|||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class ClassWithoutTypeConverter
|
||||
{
|
||||
}
|
||||
|
||||
[TypeConverter(typeof(SecretMessageTypeConverter))]
|
||||
private class SecretMessage
|
||||
{
|
||||
public string Message { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Message;
|
||||
}
|
||||
}
|
||||
|
||||
private class SecretMessageTypeConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||
{
|
||||
if (sourceType == typeof(string))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||
{
|
||||
if (value is string message)
|
||||
{
|
||||
return new SecretMessage() { Message = message, };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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 BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
|
|
@ -538,5 +539,48 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Equal("0.011", () => boundValue.Text);
|
||||
Assert.Equal("0.011", mirrorValue.GetAttribute("value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanBindTextboxGenericInt()
|
||||
{
|
||||
var target = Browser.FindElement(By.Id("textbox-generic-int"));
|
||||
var boundValue = Browser.FindElement(By.Id("textbox-generic-int-value"));
|
||||
var mirrorValue = Browser.FindElement(By.Id("textbox-generic-int-mirror"));
|
||||
Assert.Equal("-42", target.GetAttribute("value"));
|
||||
Assert.Equal("-42", boundValue.Text);
|
||||
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; value is not updated because it's not convertable.
|
||||
target.Clear();
|
||||
Browser.Equal("-42", () => boundValue.Text);
|
||||
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("42\t");
|
||||
Browser.Equal("42", () => boundValue.Text);
|
||||
Assert.Equal("42", mirrorValue.GetAttribute("value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanBindTextboxGenericGuid()
|
||||
{
|
||||
var target = Browser.FindElement(By.Id("textbox-generic-guid"));
|
||||
var boundValue = Browser.FindElement(By.Id("textbox-generic-guid-value"));
|
||||
var mirrorValue = Browser.FindElement(By.Id("textbox-generic-guid-mirror"));
|
||||
Assert.Equal("00000000-0000-0000-0000-000000000000", target.GetAttribute("value"));
|
||||
Assert.Equal("00000000-0000-0000-0000-000000000000", boundValue.Text);
|
||||
Assert.Equal("00000000-0000-0000-0000-000000000000", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; value is not updated because it's not convertable.
|
||||
target.Clear();
|
||||
Browser.Equal("00000000-0000-0000-0000-000000000000", () => boundValue.Text);
|
||||
Assert.Equal("00000000-0000-0000-0000-000000000000", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
var newValue = Guid.NewGuid().ToString();
|
||||
target.SendKeys(newValue + "\t");
|
||||
Browser.Equal(newValue, () => boundValue.Text);
|
||||
Assert.Equal(newValue, mirrorValue.GetAttribute("value"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,19 @@
|
|||
<input id="textbox-nullable-decimal-invalid-mirror" @bind="textboxNullableDecimalInvalidValue" readonly />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Generic bind (int):
|
||||
<BindGenericComponent Id="textbox-generic-int" @bind-Value="textboxGenericIntValue" />
|
||||
<span id="textbox-generic-int-value">@textboxGenericIntValue</span>
|
||||
<input id="textbox-generic-int-mirror" @bind="textboxGenericIntValue" readonly />
|
||||
</p>
|
||||
<p>
|
||||
Generic bind (guid):
|
||||
<BindGenericComponent Id="textbox-generic-guid" @bind-Value="textboxGenericGuidValue" />
|
||||
<span id="textbox-generic-guid-value">@textboxGenericGuidValue</span>
|
||||
<input id="textbox-generic-guid-mirror" @bind="textboxGenericGuidValue" readonly />
|
||||
</p>
|
||||
|
||||
<h2>Text Area</h2>
|
||||
<p>
|
||||
Initially blank:
|
||||
|
|
@ -138,36 +151,39 @@
|
|||
</p>
|
||||
|
||||
@code {
|
||||
string textboxInitiallyBlankValue = null;
|
||||
string textboxInitiallyPopulatedValue = "Hello";
|
||||
string textboxInitiallyBlankValue = null;
|
||||
string textboxInitiallyPopulatedValue = "Hello";
|
||||
|
||||
string textAreaInitiallyBlankValue = null;
|
||||
string textAreaInitiallyPopulatedValue = "Hello";
|
||||
string textAreaInitiallyBlankValue = null;
|
||||
string textAreaInitiallyPopulatedValue = "Hello";
|
||||
|
||||
bool? checkboxInitiallyNullValue = null;
|
||||
bool checkboxInitiallyUncheckedValue = false;
|
||||
bool checkboxInitiallyCheckedValue = true;
|
||||
bool? checkboxInitiallyNullValue = null;
|
||||
bool checkboxInitiallyUncheckedValue = false;
|
||||
bool checkboxInitiallyCheckedValue = true;
|
||||
|
||||
int textboxIntValue = -42;
|
||||
int? textboxNullableIntValue = null;
|
||||
long textboxLongValue = 3_000_000_000;
|
||||
long? textboxNullableLongValue = null;
|
||||
float textboxFloatValue = 3.141f;
|
||||
float? textboxNullableFloatValue = null;
|
||||
double textboxDoubleValue = 3.14159265359d;
|
||||
double? textboxNullableDoubleValue = null;
|
||||
decimal textboxDecimalValue = 0.0000000000000000000000000001M;
|
||||
decimal? textboxNullableDecimalValue = null;
|
||||
decimal textboxDecimalInvalidValue = 0.0000000000000000000000000001M;
|
||||
decimal? textboxNullableDecimalInvalidValue = null;
|
||||
int textboxIntValue = -42;
|
||||
int? textboxNullableIntValue = null;
|
||||
long textboxLongValue = 3_000_000_000;
|
||||
long? textboxNullableLongValue = null;
|
||||
float textboxFloatValue = 3.141f;
|
||||
float? textboxNullableFloatValue = null;
|
||||
double textboxDoubleValue = 3.14159265359d;
|
||||
double? textboxNullableDoubleValue = null;
|
||||
decimal textboxDecimalValue = 0.0000000000000000000000000001M;
|
||||
decimal? textboxNullableDecimalValue = null;
|
||||
decimal textboxDecimalInvalidValue = 0.0000000000000000000000000001M;
|
||||
decimal? textboxNullableDecimalInvalidValue = null;
|
||||
|
||||
bool includeFourthOption = false;
|
||||
enum SelectableValue { First, Second, Third, Fourth }
|
||||
SelectableValue selectValue = SelectableValue.Second;
|
||||
int textboxGenericIntValue = -42;
|
||||
Guid textboxGenericGuidValue = Guid.Empty;
|
||||
|
||||
void AddAndSelectNewSelectOption()
|
||||
{
|
||||
includeFourthOption = true;
|
||||
selectValue = SelectableValue.Fourth;
|
||||
}
|
||||
bool includeFourthOption = false;
|
||||
enum SelectableValue { First, Second, Third, Fourth }
|
||||
SelectableValue selectValue = SelectableValue.Second;
|
||||
|
||||
void AddAndSelectNewSelectOption()
|
||||
{
|
||||
includeFourthOption = true;
|
||||
selectValue = SelectableValue.Fourth;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
@typeparam TValue
|
||||
|
||||
<input id="@Id" type="text" value="@Value" @onchange="@(EventCallback.Factory.CreateBinder<TValue>(this, _ => ValueChanged.InvokeAsync(_), Value))" />
|
||||
|
||||
@code {
|
||||
[Parameter] string Id { get; set; }
|
||||
[Parameter] TValue Value { get; set; }
|
||||
[Parameter] EventCallback<TValue> ValueChanged { get; set; }
|
||||
}
|
||||
Loading…
Reference in New Issue