From 78616ed46073ab82132d608298e10b72bbed6a45 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Tue, 8 Feb 2022 08:54:47 -0800 Subject: [PATCH 1/5] Add new binder APIs --- .../src/CompilerServices/RuntimeHelpers.cs | 17 + .../Components/src/EventCallbackFactory.cs | 51 + .../EventCallbackFactoryBinderExtensions.cs | 923 ++++++++++++++++-- .../Components/src/PublicAPI.Unshipped.txt | 33 + .../Components/test/RendererTest.cs | 194 ++++ 5 files changed, 1124 insertions(+), 94 deletions(-) diff --git a/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs b/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs index 8404807e4c3d..d030f7fc98d9 100644 --- a/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs +++ b/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs @@ -50,4 +50,21 @@ public static EventCallback CreateInferredEventCallback(object receiver, F { return EventCallback.Factory.Create(receiver, callback); } + + /// + /// Not intended for use by application code. + /// + /// + /// + /// + /// + // + // This method is used with `@bind-Value` for components. When a component has a generic type, it's + // really messy to write to try and write the parameter type for ValueChanged - because it can contain generic + // type parameters. We're using a trick of type inference to generate the proper typing for the delegate + // so that method-group-to-delegate conversion works. + public static EventCallback CreateInferredEventCallback(object receiver, EventCallback callback, T value) + { + return EventCallback.Factory.Create(receiver, callback); + } } diff --git a/src/Components/Components/src/EventCallbackFactory.cs b/src/Components/Components/src/EventCallbackFactory.cs index 4e23584e4ba6..99263612f5c5 100644 --- a/src/Components/Components/src/EventCallbackFactory.cs +++ b/src/Components/Components/src/EventCallbackFactory.cs @@ -198,6 +198,57 @@ public EventCallback Create(object receiver, Func return CreateCore(receiver, callback); } + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback CreateSetter(object receiver, Action callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback CreateSetter(object receiver, Func callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback CreateSetter(object receiver, EventCallback callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + return callback; + } + /// /// Creates an for the provided and /// . For internal framework use only. diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs index 733c80e6e49e..f8c52979bb84 100644 --- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs +++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs @@ -42,6 +42,26 @@ public static EventCallback CreateBinder( return CreateBinderCore(factory, receiver, setter, culture, ConvertToString); } + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + string existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToString); + } + /// /// For internal use only. /// @@ -62,6 +82,17 @@ public static EventCallback CreateBinder( return CreateBinderCore(factory, receiver, setter, culture, ConvertToBool); } + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + bool existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToBool); + } + /// /// For internal use only. /// @@ -79,7 +110,611 @@ public static EventCallback CreateBinder( bool? existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableBool); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableBool); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + bool? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableBool); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + int existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToInt); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + int existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToInt); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + int? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableInt); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + int? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableInt); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + long existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToLong); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + long existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToLong); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + short existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToShort); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + short existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToShort); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + long? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableLong); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + long? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableLong); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + short? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableShort); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + short? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableShort); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + float existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToFloat); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + float existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToFloat); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + float? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableFloat); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + float? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableFloat); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + double existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDoubleDelegate); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + double existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDoubleDelegate); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + double? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDoubleDelegate); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + double? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDoubleDelegate); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + decimal existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDecimal); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + decimal existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDecimal); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + decimal? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDecimal); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + decimal? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDecimal); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + DateTime existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateTime); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + DateTime existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateTime); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + DateTime existingValue, + string format, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateTimeWithFormat); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + DateTime existingValue, + string format, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateTimeWithFormat); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + DateTime? existingValue, + CultureInfo? culture = null) + { + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateTime); } /// @@ -95,11 +730,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - int existingValue, + EventCallback setter, + DateTime? existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToInt); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateTime); } /// @@ -109,17 +744,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - int? existingValue, + Action setter, + DateTime? existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableInt); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateTimeWithFormat); } /// @@ -129,17 +766,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - long existingValue, + EventCallback setter, + DateTime? existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToLong); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateTimeWithFormat); } /// @@ -155,11 +794,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - short existingValue, + Action setter, + DateTimeOffset existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToShort); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateTimeOffset); } /// @@ -175,11 +814,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - long? existingValue, + EventCallback setter, + DateTimeOffset existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableLong); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateTimeOffset); } /// @@ -189,17 +828,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - short? existingValue, + Action setter, + DateTimeOffset existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableShort); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateTimeOffsetWithFormat); } /// @@ -209,17 +850,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - float existingValue, + EventCallback setter, + DateTimeOffset existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToFloat); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateTimeOffsetWithFormat); } /// @@ -235,11 +878,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - float? existingValue, + Action setter, + DateTimeOffset? existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableFloat); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateTimeOffset); } /// @@ -255,11 +898,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - double existingValue, + EventCallback setter, + DateTimeOffset? existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToDoubleDelegate); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateTimeOffset); } /// @@ -269,17 +912,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - double? existingValue, + Action setter, + DateTimeOffset? existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDoubleDelegate); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateTimeOffsetWithFormat); } /// @@ -289,17 +934,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - decimal existingValue, + EventCallback setter, + DateTimeOffset? existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToDecimal); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateTimeOffsetWithFormat); } /// @@ -315,11 +962,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - decimal? existingValue, + Action setter, + DateOnly existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDecimal); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateOnly); } /// @@ -335,11 +982,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateTime existingValue, + EventCallback setter, + DateOnly existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateTime); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateOnly); } /// @@ -356,12 +1003,12 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateTime existingValue, + Action setter, + DateOnly existingValue, string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateTimeWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateOnlyWithFormat); } /// @@ -371,17 +1018,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateTime? existingValue, + EventCallback setter, + DateOnly existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateTime); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateOnlyWithFormat); } /// @@ -391,19 +1040,17 @@ public static EventCallback CreateBinder( /// /// /// - /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateTime? existingValue, - string format, + Action setter, + DateOnly? existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateTimeWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateOnly); } /// @@ -419,11 +1066,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateTimeOffset existingValue, + EventCallback setter, + DateOnly? existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateTimeOffset); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateOnly); } /// @@ -440,12 +1087,12 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateTimeOffset existingValue, + Action setter, + DateOnly? existingValue, string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateTimeOffsetWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateOnlyWithFormat); } /// @@ -455,17 +1102,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateTimeOffset? existingValue, + EventCallback setter, + DateOnly? existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateTimeOffset); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateOnlyWithFormat); } /// @@ -475,19 +1124,17 @@ public static EventCallback CreateBinder( /// /// /// - /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateTimeOffset? existingValue, - string format, + Action setter, + TimeOnly existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateTimeOffsetWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToTimeOnly); } /// @@ -503,11 +1150,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateOnly existingValue, + EventCallback setter, + TimeOnly existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToDateOnly); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToTimeOnly); } /// @@ -524,12 +1171,12 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateOnly existingValue, + Action setter, + TimeOnly existingValue, string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToDateOnlyWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToTimeOnlyWithFormat); } /// @@ -539,17 +1186,19 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateOnly? existingValue, + EventCallback setter, + TimeOnly existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableDateOnly); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToTimeOnlyWithFormat); } /// @@ -559,19 +1208,17 @@ public static EventCallback CreateBinder( /// /// /// - /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - DateOnly? existingValue, - string format, + Action setter, + TimeOnly? existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableDateOnlyWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableTimeOnly); } /// @@ -587,11 +1234,11 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - TimeOnly existingValue, + EventCallback setter, + TimeOnly? existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToTimeOnly); + return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableTimeOnly); } /// @@ -608,12 +1255,12 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, - TimeOnly existingValue, + Action setter, + TimeOnly? existingValue, string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToTimeOnlyWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableTimeOnlyWithFormat); } /// @@ -623,39 +1270,40 @@ public static EventCallback CreateBinder( /// /// /// + /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, object receiver, - Action setter, + EventCallback setter, TimeOnly? existingValue, + string format, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, ConvertToNullableTimeOnly); + return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableTimeOnlyWithFormat); } /// /// For internal use only. /// + /// /// /// /// /// - /// /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static EventCallback CreateBinder( + public static EventCallback CreateBinder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( this EventCallbackFactory factory, object receiver, - Action setter, - TimeOnly? existingValue, - string format, + Action setter, + T existingValue, CultureInfo? culture = null) { - return CreateBinderCore(factory, receiver, setter, culture, format, ConvertToNullableTimeOnlyWithFormat); + return CreateBinderCore(factory, receiver, setter, culture, ParserDelegateCache.Get()); } /// @@ -672,7 +1320,7 @@ public static EventCallback CreateBinder( public static EventCallback CreateBinder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( this EventCallbackFactory factory, object receiver, - Action setter, + EventCallback setter, T existingValue, CultureInfo? culture = null) { @@ -722,6 +1370,49 @@ private static EventCallback CreateBinderCore( return factory.Create(receiver, callback); } + private static EventCallback CreateBinderCore( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + CultureInfo? culture, + BindConverter.BindParser converter) + { + Func callback = async e => + { + T? value = default; + var converted = false; + try + { + converted = converter(e.Value, culture, out value); + } + catch + { + } + + // We only invoke the setter if the conversion didn't throw, or if the newly-entered value is empty. + // If the user entered some non-empty value we couldn't parse, we leave the state of the .NET field + // unchanged, which for a two-way binding results in the UI reverting to its previous valid state + // because the diff will see the current .NET output no longer matches the render tree since we + // patched it to reflect the state of the UI. + // + // This reversion behavior is valuable because alternatives are problematic: + // - If we assigned default(T) on failure, the user would lose whatever data they were editing, + // for example if they accidentally pressed an alphabetical key while editing a number with + // @bind:event="oninput" + // - If the diff mechanism didn't revert to the previous good value, the user wouldn't necessarily + // know that the data they are submitting is different from what they think they've typed + if (converted) + { + await setter.InvokeAsync(value!); + } + else if (string.Empty.Equals(e.Value)) + { + await setter.InvokeAsync(default!); + } + }; + return factory.Create(receiver, callback); + } + private static EventCallback CreateBinderCore( this EventCallbackFactory factory, object receiver, @@ -765,4 +1456,48 @@ private static EventCallback CreateBinderCore( }; return factory.Create(receiver, callback); } + + private static EventCallback CreateBinderCore( + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + CultureInfo? culture, + string format, + BindConverter.BindParserWithFormat converter) + { + Func callback = async e => + { + T? value = default; + var converted = false; + try + { + converted = converter(e.Value, culture, format, out value); + } + catch + { + } + + // We only invoke the setter if the conversion didn't throw, or if the newly-entered value is empty. + // If the user entered some non-empty value we couldn't parse, we leave the state of the .NET field + // unchanged, which for a two-way binding results in the UI reverting to its previous valid state + // because the diff will see the current .NET output no longer matches the render tree since we + // patched it to reflect the state of the UI. + // + // This reversion behavior is valuable because alternatives are problematic: + // - If we assigned default(T) on failure, the user would lose whatever data they were editing, + // for example if they accidentally pressed an alphabetical key while editing a number with + // @bind:event="oninput" + // - If the diff mechanism didn't revert to the previous good value, the user wouldn't necessarily + // know that the data they are submitting is different from what they think they've typed + if (converted) + { + await setter.InvokeAsync(value!); + } + else if (string.Empty.Equals(e.Value)) + { + await setter.InvokeAsync(default!); + } + }; + return factory.Create(receiver, callback); + } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 53b72159add0..ff67716d1b54 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,2 +1,35 @@ #nullable enable Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddContent(int sequence, Microsoft.AspNetCore.Components.MarkupString? markupContent) -> void +static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback(object! receiver, Microsoft.AspNetCore.Components.EventCallback callback, T value) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateOnly existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateOnly existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateOnly? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateOnly? existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateTime existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateTime existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateTime? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateTime? existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateTimeOffset existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateTimeOffset existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateTimeOffset? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateTimeOffset? existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.TimeOnly existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.TimeOnly existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.TimeOnly? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.TimeOnly? existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, bool existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, bool? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, decimal existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, decimal? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, double existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, double? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, float existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, float? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, int existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, int? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, long existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, long? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, short existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, short? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, string! existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, T existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 6959ccc0d34f..f5a2eef6a651 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Globalization; using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Components.CompilerServices; using Microsoft.AspNetCore.Components.HotReload; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; @@ -3042,6 +3043,199 @@ public async Task CanCombineBindAndConditionalAttribute() await renderTask; } + [Fact] + public async Task BindWithSynchronousSetter_Lambda() + { + // Arrange + var renderer = new TestRenderer(); + var component = new OuterEventComponent(); + var value = "value"; + component.RenderFragment = (builder) => + { + builder.OpenElement(0, "input"); + builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder( + component, + RuntimeHelpers.CreateInferredEventCallback(component, __value => value = __value, value), + value)); + builder.CloseElement(); + }; + var componentId = renderer.AssignRootComponentId(component); + await component.TriggerRenderAsync(); + var checkboxChangeEventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act: Trigger change event + var eventArgs = new ChangeEventArgs { Value = "hello" }; + var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs); + await renderTask; + + // Assert + Assert.Equal("hello", value); + } + + [Fact] + public async Task BindWithAsynchronousSetter_MethodGroupToDelegate() + { + // This test represents https://github.com/dotnet/blazor/issues/624 + + // Arrange: Rendered with textbox enabled + var renderer = new TestRenderer(); + var component = new OuterEventComponent(); + var value = "value"; + async Task SetValue(string __value) + { + value = __value; + await Task.CompletedTask; + } + component.RenderFragment = (builder) => + { + builder.OpenElement(0, "input"); + builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder( + component, + RuntimeHelpers.CreateInferredEventCallback(component, SetValue, value), + value)); + builder.CloseElement(); + }; + var componentId = renderer.AssignRootComponentId(component); + await component.TriggerRenderAsync(); + var checkboxChangeEventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act: Trigger change event + var eventArgs = new ChangeEventArgs { Value = "hello" }; + var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs); + await renderTask; + + // Assert + Assert.Equal("hello", value); + } + + [Fact] + public async Task BindWithAsynchronousSetter_ExistingEventCallback() + { + // This test represents https://github.com/dotnet/blazor/issues/624 + + // Arrange: Rendered with textbox enabled + var renderer = new TestRenderer(); + var component = new OuterEventComponent(); +#nullable enable + var value = "value"; + var eventCallback = new EventCallback(component, (string? __value) => + { + value = __value; + }); +#nullable disable + component.RenderFragment = (builder) => + { + builder.OpenElement(0, "input"); + builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder( + component, + RuntimeHelpers.CreateInferredEventCallback(component, eventCallback, value), + value)); + builder.CloseElement(); + }; + var componentId = renderer.AssignRootComponentId(component); + await component.TriggerRenderAsync(); + var checkboxChangeEventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act: Trigger change event + var eventArgs = new ChangeEventArgs { Value = "hello" }; + var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs); + await renderTask; + + // Assert + Assert.Equal("hello", value); + } + + [Fact] + public async Task BindWithAfter() + { + // This test represents https://github.com/dotnet/blazor/issues/624 + + // Arrange: Rendered with textbox enabled + var renderer = new TestRenderer(); + var component = new OuterEventComponent(); + string value = "value"; + component.RenderFragment = (builder) => + { + builder.OpenElement(0, "input"); + builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder( + component, + RuntimeHelpers.CreateInferredEventCallback( + component, + async __value => + { + value = __value; + await EventCallback.Factory.Create(component, () => Task.CompletedTask).InvokeAsync(); + }, + value), + value)); + builder.CloseElement(); + }; + var componentId = renderer.AssignRootComponentId(component); + await component.TriggerRenderAsync(); + var checkboxChangeEventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act: Trigger change event + var eventArgs = new ChangeEventArgs { Value = "hello" }; + var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs); + await renderTask; + + // Assert + Assert.Equal("hello", value); + } + + [Fact] + public async Task BindWithAfter_Action() + { + // This test represents https://github.com/dotnet/blazor/issues/624 + + // Arrange: Rendered with textbox enabled + var renderer = new TestRenderer(); + var component = new OuterEventComponent(); + string value = "value"; + component.RenderFragment = (builder) => + { + builder.OpenElement(0, "input"); + builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder( + component, + RuntimeHelpers.CreateInferredEventCallback( + component, + async __value => + { + value = __value; + await EventCallback.Factory.Create(component, () => { }).InvokeAsync(); + }, + value), + value)); + builder.CloseElement(); + }; + var componentId = renderer.AssignRootComponentId(component); + await component.TriggerRenderAsync(); + var checkboxChangeEventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act: Trigger change event + var eventArgs = new ChangeEventArgs { Value = "hello" }; + var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs); + await renderTask; + + // Assert + Assert.Equal("hello", value); + } + [Fact] public void HandlesNestedElementCapturesDuringRefresh() { From 3f440e229834465a9c46da4a2a358609d247fe7f Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 11 Feb 2022 06:17:23 -0800 Subject: [PATCH 2/5] Additional runtime helpers required for component bind:after --- .../src/CompilerServices/RuntimeHelpers.cs | 57 +++++++++++++++++++ .../Components/src/EventCallbackFactory.cs | 51 ----------------- .../Components/src/PublicAPI.Unshipped.txt | 3 + 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs b/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs index d030f7fc98d9..8ae3911c4d63 100644 --- a/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs +++ b/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; + namespace Microsoft.AspNetCore.Components.CompilerServices; /// @@ -67,4 +69,59 @@ public static EventCallback CreateInferredEventCallback(object receiver, E { return EventCallback.Factory.Create(receiver, callback); } + + /// + /// Not intended for use by application code. + /// + /// + /// + // + // This method is used with `@bind-Value:after` for components. When :after is provided we don't know the + // type of the expression provided by the developer or if we can invoke it directly, as it can be a lambda + // and unlike in JavaScript, C# doesn't support Immediately Invoked Function Expressions so we need to pass + // the expression to this helper method and invoke it inside. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InvokeSynchronousDelegate(Action callback) + { + callback(); + } + + /// + /// Not intended for use by application code. + /// + /// + /// + // + // This method is used with `@bind-Value:after` for components. When :after is provided we don't know the + // type of the expression provided by the developer or if we can invoke it directly, as it can be a lambda + // and unlike in JavaScript, C# doesn't support Immediately Invoked Function Expressions so we need to pass + // the expression to this helper method and invoke it inside. + // In addition to that, when the receiving target delegate property result is awaitable, we can receive either + // an Action or a Func and we don't have that information at compile time, so we use this helper to + // normalize both operations into a Task in the same way we do for EventCallback + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Task InvokeAsynchronousDelegate(Action callback) + { + callback(); + return Task.CompletedTask; + } + + /// + /// Not intended for use by application code. + /// + /// + /// + // + // This method is used with `@bind-Value:after` for components. When :after is provided we don't know the + // type of the expression provided by the developer or if we can invoke it directly, as it can be a lambda + // and unlike in JavaScript, C# doesn't support Immediately Invoked Function Expressions so we need to pass + // the expression to this helper method and invoke it inside. + // In addition to that, when the receiving target delegate property result is awaitable, we can receive either + // an Action or a Func and we don't have that information at compile time, so we use this helper to + // normalize both operations into a Task in the same way we do for EventCallback + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Task InvokeAsynchronousDelegate(Func callback) + { + return callback(); + } } diff --git a/src/Components/Components/src/EventCallbackFactory.cs b/src/Components/Components/src/EventCallbackFactory.cs index 99263612f5c5..4e23584e4ba6 100644 --- a/src/Components/Components/src/EventCallbackFactory.cs +++ b/src/Components/Components/src/EventCallbackFactory.cs @@ -198,57 +198,6 @@ public EventCallback Create(object receiver, Func return CreateCore(receiver, callback); } - /// - /// Creates an for the provided and - /// . - /// - /// The event receiver. - /// The event callback. - /// The . - public EventCallback CreateSetter(object receiver, Action callback) - { - if (receiver == null) - { - throw new ArgumentNullException(nameof(receiver)); - } - - return CreateCore(receiver, callback); - } - - /// - /// Creates an for the provided and - /// . - /// - /// The event receiver. - /// The event callback. - /// The . - public EventCallback CreateSetter(object receiver, Func callback) - { - if (receiver == null) - { - throw new ArgumentNullException(nameof(receiver)); - } - - return CreateCore(receiver, callback); - } - - /// - /// Creates an for the provided and - /// . - /// - /// The event receiver. - /// The event callback. - /// The . - public EventCallback CreateSetter(object receiver, EventCallback callback) - { - if (receiver == null) - { - throw new ArgumentNullException(nameof(receiver)); - } - - return callback; - } - /// /// Creates an for the provided and /// . For internal framework use only. diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index ff67716d1b54..3b526beb24f9 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,6 +1,9 @@ #nullable enable Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddContent(int sequence, Microsoft.AspNetCore.Components.MarkupString? markupContent) -> void static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback(object! receiver, Microsoft.AspNetCore.Components.EventCallback callback, T value) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate(System.Action! callback) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate(System.Func! callback) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeSynchronousDelegate(System.Action! callback) -> void static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateOnly existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateOnly existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback setter, System.DateOnly? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback From b2be98c4a2c8758ff3dd776f30d4b35229ba4e76 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 11 Feb 2022 06:32:21 -0800 Subject: [PATCH 3/5] Style fixes --- .../EventCallbackFactoryBinderExtensions.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs index f8c52979bb84..753c8a99d494 100644 --- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs +++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs @@ -1371,11 +1371,11 @@ private static EventCallback CreateBinderCore( } private static EventCallback CreateBinderCore( - this EventCallbackFactory factory, - object receiver, - EventCallback setter, - CultureInfo? culture, - BindConverter.BindParser converter) + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + CultureInfo? culture, + BindConverter.BindParser converter) { Func callback = async e => { @@ -1458,12 +1458,12 @@ private static EventCallback CreateBinderCore( } private static EventCallback CreateBinderCore( - this EventCallbackFactory factory, - object receiver, - EventCallback setter, - CultureInfo? culture, - string format, - BindConverter.BindParserWithFormat converter) + this EventCallbackFactory factory, + object receiver, + EventCallback setter, + CultureInfo? culture, + string format, + BindConverter.BindParserWithFormat converter) { Func callback = async e => { From 612cbb93980b227d62b464667173d21ab5691743 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 11 Feb 2022 06:53:28 -0800 Subject: [PATCH 4/5] Add missing doc comment --- .../src/EventCallbackFactoryBinderExtensions.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs index 753c8a99d494..42328e8f8f78 100644 --- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs +++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs @@ -82,6 +82,15 @@ public static EventCallback CreateBinder( return CreateBinderCore(factory, receiver, setter, culture, ConvertToBool); } + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static EventCallback CreateBinder( this EventCallbackFactory factory, From 744c9720bb31c8e69f868b7b535e595b60db1a6b Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 22 Jun 2022 15:51:41 +0200 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Tanay Parikh --- .../Components/src/CompilerServices/RuntimeHelpers.cs | 2 +- src/Components/Components/test/RendererTest.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs b/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs index 8ae3911c4d63..8abdc5faf0b7 100644 --- a/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs +++ b/src/Components/Components/src/CompilerServices/RuntimeHelpers.cs @@ -62,7 +62,7 @@ public static EventCallback CreateInferredEventCallback(object receiver, F /// // // This method is used with `@bind-Value` for components. When a component has a generic type, it's - // really messy to write to try and write the parameter type for ValueChanged - because it can contain generic + // really messy to write the parameter type for ValueChanged - because it can contain generic // type parameters. We're using a trick of type inference to generate the proper typing for the delegate // so that method-group-to-delegate conversion works. public static EventCallback CreateInferredEventCallback(object receiver, EventCallback callback, T value) diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index f5a2eef6a651..164dc17e3978 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -3054,9 +3054,9 @@ public async Task BindWithSynchronousSetter_Lambda() { builder.OpenElement(0, "input"); builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder( - component, - RuntimeHelpers.CreateInferredEventCallback(component, __value => value = __value, value), - value)); + component, + RuntimeHelpers.CreateInferredEventCallback(component, __value => value = __value, value), + value)); builder.CloseElement(); }; var componentId = renderer.AssignRootComponentId(component);