From 5af8e170bc11aa189f7be0aab777b1bdfb76a431 Mon Sep 17 00:00:00 2001
From: Ryan Nowak
Date: Tue, 4 Jun 2019 17:31:01 -0700
Subject: [PATCH] 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
---
.../targets/BuiltInBclLinkerDescriptor.xml | 4 +
...ft.AspNetCore.Components.netstandard2.0.cs | 3 +-
.../EventCallbackFactoryBinderExtensions.cs | 341 ++++++++++++++----
...ventCallbackFactoryBinderExtensionsTest.cs | 152 ++++++++
src/Components/test/E2ETest/Tests/BindTest.cs | 44 +++
.../BasicTestApp/BindCasesComponent.razor | 70 ++--
.../BasicTestApp/BindGenericComponent.razor | 9 +
7 files changed, 519 insertions(+), 104 deletions(-)
create mode 100644 src/Components/test/testassets/BasicTestApp/BindGenericComponent.razor
diff --git a/src/Components/Blazor/Build/src/targets/BuiltInBclLinkerDescriptor.xml b/src/Components/Blazor/Build/src/targets/BuiltInBclLinkerDescriptor.xml
index 4b442b1bb8..32533df8ca 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 382346efce..7bdacfbc4b 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 @@ namespace Microsoft.AspNetCore.Components
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 @@ namespace Microsoft.AspNetCore.Components
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 ba0a23dd0a..16fe53ad26 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 @@ 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 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;
-
- 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(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(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 @@ namespace Microsoft.AspNetCore.Components
Action setter,
string existingValue)
{
- ;
return CreateBinderCore(factory, receiver, setter, ConvertToString);
}
@@ -489,15 +557,6 @@ namespace Microsoft.AspNetCore.Components
Action setter,
decimal? existingValue)
{
- Func