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:
Ryan Nowak 2019-06-04 17:31:01 -07:00 committed by GitHub
parent 03d94e0ee6
commit 5af8e170bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 519 additions and 104 deletions

View File

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

View File

@ -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
{

View File

@ -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;
}
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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"));
}
}
}

View File

@ -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;
}
}

View File

@ -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; }
}