diff --git a/src/Components/Blazor/Build/src/targets/BuiltInBclLinkerDescriptor.xml b/src/Components/Blazor/Build/src/targets/BuiltInBclLinkerDescriptor.xml index 4b442b1bb87e..32533df8ca3c 100644 --- a/src/Components/Blazor/Build/src/targets/BuiltInBclLinkerDescriptor.xml +++ b/src/Components/Blazor/Build/src/targets/BuiltInBclLinkerDescriptor.xml @@ -13,5 +13,9 @@ + + + + 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 382346efce24..7bdacfbc4b0d 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -155,6 +155,7 @@ public static partial class EventCallbackFactoryBinderExtensions public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, int existingValue) { throw null; } public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, long existingValue) { throw null; } public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, bool? existingValue) { throw null; } + public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, System.DateTime? existingValue) { throw null; } public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, decimal? existingValue) { throw null; } public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, double? existingValue) { throw null; } public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, int? existingValue) { throw null; } @@ -162,7 +163,7 @@ public static partial class EventCallbackFactoryBinderExtensions public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, float? existingValue) { throw null; } public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, float existingValue) { throw null; } public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, string existingValue) { throw null; } - public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, T existingValue) where T : struct, System.Enum { throw null; } + public static Microsoft.AspNetCore.Components.EventCallback CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action setter, T existingValue) { throw null; } } public static partial class EventCallbackFactoryUIEventArgsExtensions { diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs index ba0a23dd0a96..16fe53ad2614 100644 --- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs +++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs @@ -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 { /// /// Contains extension methods for two-way binding using . For internal use only. /// + // + // NOTE: for number parsing, the HTML5 spec dictates that 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(object obj, out T value); @@ -53,7 +67,7 @@ private static bool ConvertToIntCore(object obj, out int value) 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 @@ private static bool ConvertToNullableIntCore(object obj, out int? value) 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 @@ private static bool ConvertToLongCore(object obj, out long value) 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 @@ private static bool ConvertToNullableLongCore(object obj, out long? value) 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 @@ private static bool ConvertToFloatCore(object obj, out float value) 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 @@ private static bool ConvertToNullableFloatCore(object obj, out float? value) 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 @@ private static bool ConvertToDoubleCore(object obj, out double value) 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 @@ private static bool ConvertToNullableDoubleCore(object obj, out double? value) 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 @@ private static bool ConvertToDecimalCore(object obj, out decimal value) 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 @@ private static bool ConvertToNullableDecimalCore(object obj, out decimal? value) 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 @@ private static bool ConvertToNullableDecimalCore(object obj, out decimal? value) return true; } - private static class EnumConverter where T : struct, Enum + private static BindConverter ConvertToDateTime = ConvertToDateTimeCore; + private static BindConverter ConvertToNullableDateTime = ConvertToNullableDateTimeCore; + + private static bool ConvertToDateTimeCore(object obj, out DateTime value) { - public static readonly BindConverter Convert = ConvertCore; + var text = (string)obj; + if (string.IsNullOrEmpty(text)) + { + value = default; + return false; + } - public static bool ConvertCore(object obj, out T value) + if (!DateTime.TryParse(text, CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted)) { - var text = (string)obj; - if (string.IsNullOrEmpty(text)) - { - value = default; - return true; - } + value = default; + return false; + } - if (!Enum.TryParse(text, out var converted)) - { - value = default; - return false; - } + value = converted; + return true; + } - value = converted; + 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(object obj, 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, out Nullable 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; } /// @@ -284,7 +353,6 @@ public static EventCallback CreateBinder( Action setter, string existingValue) { - ; return CreateBinderCore(factory, receiver, setter, ConvertToString); } @@ -489,15 +557,6 @@ public static EventCallback CreateBinder( Action setter, decimal? existingValue) { - Func converter = (obj) => - { - if (decimal.TryParse((string)obj, out var value)) - { - return value; - } - - return null; - }; return CreateBinderCore(factory, receiver, setter, ConvertToNullableDecimal); } @@ -515,28 +574,24 @@ public static EventCallback CreateBinder( Action setter, DateTime existingValue) { - // Avoiding CreateBinderCore so we can avoid an extra allocating lambda - // when a format is used. - Action callback = (e) => - { - DateTime value = default; - var converted = false; - try - { - value = ConvertDateTime(e.Value, format: null); - converted = true; - } - catch - { - } + return CreateBinderCore(factory, receiver, setter, ConvertToDateTime); + } - // See comments in CreateBinderCore - if (converted) - { - setter(value); - } - }; - return factory.Create(receiver, callback); + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + DateTime? existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToNullableDateTime); } /// @@ -577,6 +632,23 @@ public static EventCallback CreateBinder( } }; return factory.Create(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); + } + } } /// @@ -592,26 +664,9 @@ public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, Action setter, - T existingValue) where T : struct, Enum + T existingValue) { - return CreateBinderCore(factory, receiver, setter, EnumConverter.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(factory, receiver, setter, BinderConverterCache.Get()); } private static EventCallback CreateBinderCore( @@ -642,5 +697,139 @@ private static EventCallback CreateBinderCore( }; 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).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, 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; + } + } + } } } diff --git a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs index 6779401282e3..84bfc725074e 100644 --- a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs +++ b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs @@ -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 @@ public async Task CreateBinder_Enum() Assert.Equal(1, component.Count); } + [Fact] + public async Task CreateBinder_NullableEnum() + { + // Arrange + var value = (AttributeTargets?)AttributeTargets.All; + var component = new EventCountingComponent(); + Action 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 @@ public async Task CreateBinder_DateTime() Assert.Equal(1, component.Count); } + [Fact] + public async Task CreateBinder_NullableDateTime() + { + // Arrange + var value = (DateTime?)DateTime.Now; + var component = new EventCountingComponent(); + Action 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 @@ public async Task CreateBinder_DateTime_Format() 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 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 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 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 setter = (_) => value = _; + + var ex = Assert.Throws(() => 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 @@ public Task SetParametersAsync(ParameterCollection parameters) 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; + } + } } } diff --git a/src/Components/test/E2ETest/Tests/BindTest.cs b/src/Components/test/E2ETest/Tests/BindTest.cs index 51fcc9ea9d1d..4931dd786ec4 100644 --- a/src/Components/test/E2ETest/Tests/BindTest.cs +++ b/src/Components/test/E2ETest/Tests/BindTest.cs @@ -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 @@ public void CanBindTextbox_NullableDecimal_InvalidInput() 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")); + } } } diff --git a/src/Components/test/testassets/BasicTestApp/BindCasesComponent.razor b/src/Components/test/testassets/BasicTestApp/BindCasesComponent.razor index 362db2936f3a..a8b393660b09 100644 --- a/src/Components/test/testassets/BasicTestApp/BindCasesComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/BindCasesComponent.razor @@ -90,6 +90,19 @@

+

+ Generic bind (int): + + @textboxGenericIntValue + +

+

+ Generic bind (guid): + + @textboxGenericGuidValue + +

+

Text Area

Initially blank: @@ -138,36 +151,39 @@

@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; +int textboxGenericIntValue = -42; +Guid textboxGenericGuidValue = Guid.Empty; - bool includeFourthOption = false; - enum SelectableValue { First, Second, Third, Fourth } - SelectableValue selectValue = SelectableValue.Second; +bool includeFourthOption = false; +enum SelectableValue { First, Second, Third, Fourth } +SelectableValue selectValue = SelectableValue.Second; - void AddAndSelectNewSelectOption() - { - includeFourthOption = true; - selectValue = SelectableValue.Fourth; - } +void AddAndSelectNewSelectOption() +{ + includeFourthOption = true; + selectValue = SelectableValue.Fourth; +} } diff --git a/src/Components/test/testassets/BasicTestApp/BindGenericComponent.razor b/src/Components/test/testassets/BasicTestApp/BindGenericComponent.razor new file mode 100644 index 000000000000..54072b918eb7 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/BindGenericComponent.razor @@ -0,0 +1,9 @@ +@typeparam TValue + + + +@code { + [Parameter] string Id { get; set; } + [Parameter] TValue Value { get; set; } + [Parameter] EventCallback ValueChanged { get; set; } +}