From de3506f8277db92f755a1c63f212972c2025ba2f Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 13:05:48 +0200 Subject: [PATCH 01/14] Add SupplyParameterFromForm attribute --- .../Components/src/PublicAPI.Unshipped.txt | 4 ++++ .../src/SupplyParameterFromFromAttribute.cs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/Components/Components/src/SupplyParameterFromFromAttribute.cs diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index a03797860d22..1e85e31a4501 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -56,6 +56,10 @@ Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParamete Microsoft.AspNetCore.Components.StreamRenderingAttribute Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string? +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.SupplyParameterFromFormAttribute() -> void override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int diff --git a/src/Components/Components/src/SupplyParameterFromFromAttribute.cs b/src/Components/Components/src/SupplyParameterFromFromAttribute.cs new file mode 100644 index 000000000000..453c8f1026df --- /dev/null +++ b/src/Components/Components/src/SupplyParameterFromFromAttribute.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +/// +/// Indicates that routing components may supply a value for the parameter from the +/// current URL querystring. They may also supply further values if the URL querystring changes. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class SupplyParameterFromFormAttribute : Attribute +{ + /// + /// Gets or sets the name of the querystring parameter. If null, the querystring + /// parameter is assumed to have the same name as the associated property. + /// + public string? Name { get; set; } +} From 39353404f8a72e591d57e76bbad1519ea44b3b9b Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 13:13:42 +0200 Subject: [PATCH 02/14] Detect [SupplyParameterFromFormAttribute] as a cascading value --- .../Components/src/CascadingParameterState.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index 52caad659d0b..81c888c7e04b 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -98,16 +98,24 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet var attribute = prop.GetCustomAttribute(); if (attribute != null) { - if (result == null) - { - result = new List(); - } + result ??= new List(); result.Add(new ReflectedCascadingParameterInfo( prop.Name, prop.PropertyType, attribute.Name)); } + + var fromForm = prop.GetCustomAttribute(); + if (fromForm != null) + { + result ??= new List(); + + result.Add(new ReflectedCascadingParameterInfo( + prop.Name, + prop.PropertyType, + fromForm.Name)); + } } return result?.ToArray() ?? Array.Empty(); From 4b7b6346657e00ce70b3ce44ef2f79eff97ed85b Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 14:22:02 +0200 Subject: [PATCH 03/14] Define form binder interface and basic implementation --- .../src/Binding/CascadingModelBinder.cs | 69 +++++++++++++- .../src/Binding/IFormBinderProvider.cs | 29 ++++++ .../src/Binding/ModelBindingContext.cs | 3 + .../Components/src/CascadingParameterState.cs | 7 +- .../Components/src/PublicAPI.Unshipped.txt | 6 ++ .../src/Reflection/ComponentProperties.cs | 13 ++- .../src/Binding/ClosedGenericMatcher.cs | 82 ++++++++++++++++ .../src/Binding/DefaultFormBinderProvider.cs | 95 +++++++++++++++++++ ...orComponentsServiceCollectionExtensions.cs | 2 + .../Samples/BlazorUnitedApp/Pages/Index.razor | 16 ++-- ...rosoft.AspNetCore.Components.Server.csproj | 7 +- 11 files changed, 303 insertions(+), 26 deletions(-) create mode 100644 src/Components/Components/src/Binding/IFormBinderProvider.cs create mode 100644 src/Components/Endpoints/src/Binding/ClosedGenericMatcher.cs create mode 100644 src/Components/Endpoints/src/Binding/DefaultFormBinderProvider.cs diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index 4a6b842cc4f9..cf4643bb93e1 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection.Metadata; +using Microsoft.AspNetCore.Components.Binding; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components; @@ -9,11 +11,12 @@ namespace Microsoft.AspNetCore.Components; /// /// Defines the binding context for data bound from external sources. /// -public sealed class CascadingModelBinder : IComponent, IDisposable +public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, IDisposable { private RenderHandle _handle; private ModelBindingContext? _bindingContext; private bool _hasPendingQueuedRender; + private BindingInfo? _bindingInfo; /// /// The binding context name. @@ -37,6 +40,8 @@ public sealed class CascadingModelBinder : IComponent, IDisposable [Inject] private NavigationManager Navigation { get; set; } = null!; + [Inject] private IFormBinderProvider Binder { get; set; } + void IComponent.Attach(RenderHandle renderHandle) { _handle = renderHandle; @@ -103,7 +108,7 @@ private void UpdateBindingInformation(string url) // 3) Parent has a name "parent-name" // Name = "parent-name.my-handler"; // BindingContextId = <>((<>&)|?)handler=my-handler - var name = string.IsNullOrEmpty(ParentContext?.Name) ? Name : $"{ParentContext.Name}.{Name}"; + var name = ModelBindingContext.Combine(ParentContext, Name); var bindingId = string.IsNullOrEmpty(name) ? "" : GenerateBindingContextId(name); var bindingContext = _bindingContext != null && @@ -136,4 +141,64 @@ void IDisposable.Dispose() { Navigation.LocationChanged -= HandleLocationChanged; } + + bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName) + { + var formName = string.IsNullOrEmpty(valueName) ? + (_bindingContext?.Name) : + ModelBindingContext.Combine(_bindingContext, valueName); + + if (_bindingInfo != null && + string.Equals(_bindingInfo.FormName, formName, StringComparison.Ordinal) && + _bindingInfo.ValueType.Equals(valueType)) + { + // We already bound the value, but some component might have been destroyed and + // re-created. If the type and name of the value that we bound are the same, + // we can provide the value that we bound. + return true; + } + + // Can't supply the value if this context is for a form with a different name. + if (Binder.CanBind(formName, valueType)) + { + var bindingSucceeded = Binder.TryBind(formName, valueType, out var boundValue); + _bindingInfo = new BindingInfo(formName, valueType) + { + BindingResult = bindingSucceeded, + BoundValue = boundValue, + }; + + if (!bindingSucceeded) + { + // Report errors + } + + return true; + } + + return false; + } + + void ICascadingValueComponent.Subscribe(ComponentState subscriber) + { + throw new InvalidOperationException("Form values are always fixed."); + } + + void ICascadingValueComponent.Unsubscribe(ComponentState subscriber) + { + throw new InvalidOperationException("Form values are always fixed."); + } + + object? ICascadingValueComponent.CurrentValue => _bindingInfo == null ? + throw new InvalidOperationException("Tried to access form value before it was bound.") : + _bindingInfo.BoundValue; + + bool ICascadingValueComponent.CurrentValueIsFixed => true; + + private record BindingInfo(string? FormName, Type ValueType) + { + public required bool BindingResult { get; set; } + + public required object? BoundValue { get; set; } + } } diff --git a/src/Components/Components/src/Binding/IFormBinderProvider.cs b/src/Components/Components/src/Binding/IFormBinderProvider.cs new file mode 100644 index 000000000000..e254ddc072b5 --- /dev/null +++ b/src/Components/Components/src/Binding/IFormBinderProvider.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNetCore.Components.Binding; + +/// +/// Binds form data valuesto a model. +/// +public interface IFormBinderProvider +{ + /// + /// Determines whether the specified value type can be bound. + /// + /// The form name to bind data from. + /// The for the value to bind. + /// + bool CanBind(string formName, Type valueType); + + /// + /// Tries to bind the form with the specified name to a value of the specified type. + /// + /// The form name to bind data from. + /// The for the value to bind. + /// The bound value if succeeded. + /// true if the form was bound successfully; otherwise, false. + bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue); +} diff --git a/src/Components/Components/src/Binding/ModelBindingContext.cs b/src/Components/Components/src/Binding/ModelBindingContext.cs index 0659b325d8e1..653ffdcf068d 100644 --- a/src/Components/Components/src/Binding/ModelBindingContext.cs +++ b/src/Components/Components/src/Binding/ModelBindingContext.cs @@ -34,4 +34,7 @@ internal ModelBindingContext(string name, string bindingContextId) /// The computed identifier used to determine what parts of the app can bind data. /// public string BindingContextId { get; } + + internal static string Combine(ModelBindingContext? parentContext, string name) => + string.IsNullOrEmpty(parentContext?.Name) ? name : $"{parentContext.Name}.{name}"; } diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index 81c888c7e04b..e392cd4ac80d 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -45,11 +45,8 @@ public static IReadOnlyList FindCascadingParameters(Com var supplier = GetMatchingCascadingValueSupplier(info, componentState); if (supplier != null) { - if (resultStates == null) - { - // Although not all parameters might be matched, we know the maximum number - resultStates = new List(infos.Length - infoIndex); - } + // Although not all parameters might be matched, we know the maximum number + resultStates ??= new List(infos.Length - infoIndex); resultStates.Add(new CascadingParameterState(info.ConsumerValueName, supplier)); } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 1e85e31a4501..60106c9ac0ea 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,5 +1,8 @@ #nullable enable abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! +Microsoft.AspNetCore.Components.Binding.IFormBinderProvider +Microsoft.AspNetCore.Components.Binding.IFormBinderProvider.CanBind(string! formName, System.Type! valueType) -> bool +Microsoft.AspNetCore.Components.Binding.IFormBinderProvider.TryBind(string! formName, System.Type! valueType, out object? boundValue) -> bool Microsoft.AspNetCore.Components.CascadingModelBinder Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment! @@ -10,6 +13,9 @@ Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string! Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.IComponentRenderMode +Microsoft.AspNetCore.Components.IFormBinderProvider +Microsoft.AspNetCore.Components.IFormBinderProvider.CanBind(string! formName, System.Type! valueType) -> bool +Microsoft.AspNetCore.Components.IFormBinderProvider.TryBind(string! formName, System.Type! valueType, out object! boundValue) -> bool Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.ModelBindingContext Microsoft.AspNetCore.Components.ModelBindingContext.BindingContextId.get -> string! diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index dd0df1e094cd..e7e69ff09927 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -168,11 +168,14 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem var propertyInfo = targetType.GetProperty(parameterName, BindablePropertyFlags); if (propertyInfo != null) { - if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && !propertyInfo.IsDefined(typeof(CascadingParameterAttribute))) + if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && + !propertyInfo.IsDefined(typeof(CascadingParameterAttribute)) && + !propertyInfo.IsDefined(typeof(SupplyParameterFromFormAttribute))) { throw new InvalidOperationException( $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + - $"but it does not have [{nameof(ParameterAttribute)}] or [{nameof(CascadingParameterAttribute)}] applied."); + $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or " + + $"[{nameof(SupplyParameterFromFormAttribute)}] applied."); } else { @@ -259,7 +262,9 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) { var parameterAttribute = propertyInfo.GetCustomAttribute(); var cascadingParameterAttribute = propertyInfo.GetCustomAttribute(); - var isParameter = parameterAttribute != null || cascadingParameterAttribute != null; + var supplyParameterFromFormAttribute = propertyInfo.GetCustomAttribute(); + + var isParameter = parameterAttribute != null || cascadingParameterAttribute != null || supplyParameterFromFormAttribute != null; if (!isParameter) { continue; @@ -274,7 +279,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) var propertySetter = new PropertySetter(targetType, propertyInfo) { - Cascading = cascadingParameterAttribute != null, + Cascading = cascadingParameterAttribute != null || supplyParameterFromFormAttribute != null, }; if (_underlyingWriters.ContainsKey(propertyName)) diff --git a/src/Components/Endpoints/src/Binding/ClosedGenericMatcher.cs b/src/Components/Endpoints/src/Binding/ClosedGenericMatcher.cs new file mode 100644 index 000000000000..f455ffac018f --- /dev/null +++ b/src/Components/Endpoints/src/Binding/ClosedGenericMatcher.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +// TODO: This is shared from MVC. We should move it to a shared location. +internal static class ClosedGenericMatcher +{ + public static Type? ExtractGenericInterface(Type queryType, Type interfaceType) + { + ArgumentNullException.ThrowIfNull(queryType); + ArgumentNullException.ThrowIfNull(interfaceType); + + if (IsGenericInstantiation(queryType, interfaceType)) + { + // queryType matches (i.e. is a closed generic type created from) the open generic type. + return queryType; + } + + // Otherwise check all interfaces the type implements for a match. + // - If multiple different generic instantiations exists, we want the most derived one. + // - If that doesn't break the tie, then we sort alphabetically so that it's deterministic. + // + // We do this by looking at interfaces on the type, and recursing to the base type + // if we don't find any matches. + return GetGenericInstantiation(queryType, interfaceType); + } + + private static bool IsGenericInstantiation(Type candidate, Type interfaceType) + { + return + candidate.IsGenericType && + candidate.GetGenericTypeDefinition() == interfaceType; + } + + [SuppressMessage( + "Trimming", + "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations.", + Justification = "All bindable types are meant to be defined in assemblies where those types are preserved.")] + private static Type? GetGenericInstantiation(Type queryType, Type interfaceType) + { + Type? bestMatch = null; + var interfaces = queryType.GetInterfaces(); + foreach (var @interface in interfaces) + { + if (IsGenericInstantiation(@interface, interfaceType)) + { + if (bestMatch == null) + { + bestMatch = @interface; + } + else if (StringComparer.Ordinal.Compare(@interface.FullName, bestMatch.FullName) < 0) + { + bestMatch = @interface; + } + else + { + // There are two matches at this level of the class hierarchy, but @interface is after + // bestMatch in the sort order. + } + } + } + + if (bestMatch != null) + { + return bestMatch; + } + + // BaseType will be null for object and interfaces, which means we've reached 'bottom'. + var baseType = queryType?.BaseType; + if (baseType == null) + { + return null; + } + else + { + return GetGenericInstantiation(baseType, interfaceType); + } + } +} diff --git a/src/Components/Endpoints/src/Binding/DefaultFormBinderProvider.cs b/src/Components/Endpoints/src/Binding/DefaultFormBinderProvider.cs new file mode 100644 index 000000000000..f621ae7e43d3 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/DefaultFormBinderProvider.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using Microsoft.AspNetCore.Components.Binding; +using Microsoft.AspNetCore.Components.Forms; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal class DefaultFormBinderProvider : IFormBinderProvider +{ + private readonly FormDataProvider _formData; + // Note: This won't be implemented this way. + private Dictionary _cache = new(); + + private delegate bool TryParseInvoker(string value, IFormatProvider formatProvider, out object result); + + public DefaultFormBinderProvider(FormDataProvider formData) + { + _formData = formData; + } + + public bool CanBind(string formName, Type valueType) + { + return _formData.IsFormDataAvailable && + string.Equals(formName, _formData.Name, StringComparison.Ordinal) && + valueType == typeof(string); + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) + { + if (!CanBind(formName, valueType)) + { + boundValue = null; + return false; + } + + if (!_formData.Entries.TryGetValue("value", out var rawValue) || rawValue.Count != 1) + { + boundValue = null; + return false; + } + + var valueAsString = rawValue.ToString(); + + if (valueType == typeof(string)) + { + boundValue = valueAsString; + return true; + } + + var iParsable = ClosedGenericMatcher.ExtractGenericInterface(typeof(IParsable<>), valueType); + if (iParsable != null) + { + var method = ResolveIParsableTryParse(iParsable, valueType); + var parameters = new object[3]; + parameters[0] = valueAsString; + parameters[1] = CultureInfo.CurrentCulture; + var result = method.Invoke(null, parameters); + boundValue = parameters[2]; + + return result != null && (bool)result; + } + boundValue = null; + return false; + } + + internal static MethodInfo ResolveIParsableTryParse(Type type, Type parsable) + { + var map = type.GetInterfaceMap(parsable); + for (var i = 0; i < map.TargetMethods.Length; i++) + { + var method = map.TargetMethods[i]; + var methodNameStart = method.Name.LastIndexOf('.') + 1; + if (method.Name.AsSpan()[methodNameStart..].Equals( + nameof(IParsable.TryParse), + StringComparison.Ordinal)) + { + var parameters = method.GetParameters(); + if (parameters.Length == 3 && + parameters[0].ParameterType == typeof(string) && + parameters[1].ParameterType == typeof(IFormatProvider) && + parameters[2].ParameterType == type.MakeByRefType()) + { + return method; + } + } + } + + throw new InvalidOperationException($"Unable to resolve TryParse(string s, IFormatProvider, out T result) for type '{type.FullName}'"); + } +} diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 8be227d6cca8..3de07248dfcb 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Endpoints; using Microsoft.AspNetCore.Components.Endpoints.DependencyInjection; using Microsoft.AspNetCore.Components.Forms; @@ -56,6 +57,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection // Form handling services.TryAddScoped(); + services.TryAddScoped(); return new DefaultRazorComponentsBuilder(services); } diff --git a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor index 1684adfe5462..d8e88701afff 100644 --- a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor +++ b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor @@ -2,16 +2,12 @@ Index -

Hello, world!

- -@* - - - - - - *@ +

@value

+ + + + @if (_submitted) { @@ -19,6 +15,8 @@ } @code{ + [SupplyParameterFromForm] string value { get; set; } = "Hello, world!"; + bool _submitted = false; public void Submit() { diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index 942fc3b663e2..c6ac0f082ce5 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -37,12 +37,7 @@ - + From c419f21c40e23bbcb7c4b50983e4a359feb12eb8 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 16:54:18 +0200 Subject: [PATCH 04/14] Cleanup --- .../src/Binding/CascadingModelBinder.cs | 18 +--- ...inderProvider.cs => IFormValueSupplier.cs} | 2 +- .../Components/src/PublicAPI.Unshipped.txt | 11 +-- .../test/CascadingModelBinderTest.cs | 2 - .../src/Binding/ClosedGenericMatcher.cs | 82 ---------------- .../src/Binding/DefaultFormBinderProvider.cs | 95 ------------------- .../src/Binding/DefaultFormValuesSupplier.cs | 52 ++++++++++ ...orComponentsServiceCollectionExtensions.cs | 2 +- 8 files changed, 62 insertions(+), 202 deletions(-) rename src/Components/Components/src/Binding/{IFormBinderProvider.cs => IFormValueSupplier.cs} (96%) delete mode 100644 src/Components/Endpoints/src/Binding/ClosedGenericMatcher.cs delete mode 100644 src/Components/Endpoints/src/Binding/DefaultFormBinderProvider.cs create mode 100644 src/Components/Endpoints/src/Binding/DefaultFormValuesSupplier.cs diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index cf4643bb93e1..d863d4c5da4d 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -23,14 +23,6 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, /// [Parameter] public string Name { get; set; } = ""; - /// - /// If true, indicates that will not change. - /// This is a performance optimization that allows the framework to skip setting up - /// change notifications. Set this flag only if you will not change - /// of this context or its parents' context during the component's lifetime. - /// - [Parameter] public bool IsFixed { get; set; } - /// /// Specifies the content to be rendered inside this . /// @@ -40,7 +32,7 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, [Inject] private NavigationManager Navigation { get; set; } = null!; - [Inject] private IFormBinderProvider Binder { get; set; } + [Inject] private IFormValueSupplier FormValueSupplier { get; set; } = null!; void IComponent.Attach(RenderHandle renderHandle) { @@ -78,7 +70,7 @@ private void Render() { _hasPendingQueuedRender = false; builder.OpenComponent>(0); - builder.AddComponentParameter(1, nameof(CascadingValue.IsFixed), IsFixed); + builder.AddComponentParameter(1, nameof(CascadingValue.IsFixed), true); builder.AddComponentParameter(2, nameof(CascadingValue.Value), _bindingContext); builder.AddComponentParameter(3, nameof(CascadingValue.ChildContent), ChildContent?.Invoke(_bindingContext!)); builder.CloseComponent(); @@ -117,7 +109,7 @@ private void UpdateBindingInformation(string url) _bindingContext : new ModelBindingContext(name, bindingId); // It doesn't matter that we don't check IsFixed, since the CascadingValue we are setting up will throw if the app changes. - if (IsFixed && _bindingContext != null && _bindingContext != bindingContext) + if (_bindingContext != null && _bindingContext != bindingContext) { // Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized // as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations: @@ -159,9 +151,9 @@ bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName) } // Can't supply the value if this context is for a form with a different name. - if (Binder.CanBind(formName, valueType)) + if (FormValueSupplier.CanBind(formName!, valueType)) { - var bindingSucceeded = Binder.TryBind(formName, valueType, out var boundValue); + var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue); _bindingInfo = new BindingInfo(formName, valueType) { BindingResult = bindingSucceeded, diff --git a/src/Components/Components/src/Binding/IFormBinderProvider.cs b/src/Components/Components/src/Binding/IFormValueSupplier.cs similarity index 96% rename from src/Components/Components/src/Binding/IFormBinderProvider.cs rename to src/Components/Components/src/Binding/IFormValueSupplier.cs index e254ddc072b5..90ddd6a86650 100644 --- a/src/Components/Components/src/Binding/IFormBinderProvider.cs +++ b/src/Components/Components/src/Binding/IFormValueSupplier.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.Binding; /// /// Binds form data valuesto a model. /// -public interface IFormBinderProvider +public interface IFormValueSupplier { /// /// Determines whether the specified value type can be bound. diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 60106c9ac0ea..eb1dc00c331f 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,21 +1,16 @@ #nullable enable abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! -Microsoft.AspNetCore.Components.Binding.IFormBinderProvider -Microsoft.AspNetCore.Components.Binding.IFormBinderProvider.CanBind(string! formName, System.Type! valueType) -> bool -Microsoft.AspNetCore.Components.Binding.IFormBinderProvider.TryBind(string! formName, System.Type! valueType, out object? boundValue) -> bool +Microsoft.AspNetCore.Components.Binding.IFormValueSupplier +Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanBind(string! formName, System.Type! valueType) -> bool +Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.TryBind(string! formName, System.Type! valueType, out object? boundValue) -> bool Microsoft.AspNetCore.Components.CascadingModelBinder Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment! Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.set -> void -Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.get -> bool -Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.set -> void Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string! Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.IComponentRenderMode -Microsoft.AspNetCore.Components.IFormBinderProvider -Microsoft.AspNetCore.Components.IFormBinderProvider.CanBind(string! formName, System.Type! valueType) -> bool -Microsoft.AspNetCore.Components.IFormBinderProvider.TryBind(string! formName, System.Type! valueType, out object! boundValue) -> bool Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.ModelBindingContext Microsoft.AspNetCore.Components.ModelBindingContext.BindingContextId.get -> string! diff --git a/src/Components/Components/test/CascadingModelBinderTest.cs b/src/Components/Components/test/CascadingModelBinderTest.cs index e8a60445b8c9..dfb659231940 100644 --- a/src/Components/Components/test/CascadingModelBinderTest.cs +++ b/src/Components/Components/test/CascadingModelBinderTest.cs @@ -198,7 +198,6 @@ public void Throws_WhenIsFixedAndNameChanges() { builder.OpenComponent(0); builder.AddAttribute(1, nameof(CascadingModelBinder.Name), contextName); - builder.AddAttribute(2, nameof(CascadingModelBinder.IsFixed), true); builder.AddAttribute(3, nameof(CascadingModelBinder.ChildContent), contents); builder.CloseComponent(); }); @@ -222,7 +221,6 @@ public void Throws_WhenIsFixed_Changes(bool isFixed) var testComponent = new TestComponent(builder => { builder.OpenComponent(0); - builder.AddAttribute(1, nameof(CascadingModelBinder.IsFixed), isFixed); builder.AddAttribute(2, nameof(CascadingModelBinder.ChildContent), contents); builder.CloseComponent(); }); diff --git a/src/Components/Endpoints/src/Binding/ClosedGenericMatcher.cs b/src/Components/Endpoints/src/Binding/ClosedGenericMatcher.cs deleted file mode 100644 index f455ffac018f..000000000000 --- a/src/Components/Endpoints/src/Binding/ClosedGenericMatcher.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNetCore.Components.Endpoints; - -// TODO: This is shared from MVC. We should move it to a shared location. -internal static class ClosedGenericMatcher -{ - public static Type? ExtractGenericInterface(Type queryType, Type interfaceType) - { - ArgumentNullException.ThrowIfNull(queryType); - ArgumentNullException.ThrowIfNull(interfaceType); - - if (IsGenericInstantiation(queryType, interfaceType)) - { - // queryType matches (i.e. is a closed generic type created from) the open generic type. - return queryType; - } - - // Otherwise check all interfaces the type implements for a match. - // - If multiple different generic instantiations exists, we want the most derived one. - // - If that doesn't break the tie, then we sort alphabetically so that it's deterministic. - // - // We do this by looking at interfaces on the type, and recursing to the base type - // if we don't find any matches. - return GetGenericInstantiation(queryType, interfaceType); - } - - private static bool IsGenericInstantiation(Type candidate, Type interfaceType) - { - return - candidate.IsGenericType && - candidate.GetGenericTypeDefinition() == interfaceType; - } - - [SuppressMessage( - "Trimming", - "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations.", - Justification = "All bindable types are meant to be defined in assemblies where those types are preserved.")] - private static Type? GetGenericInstantiation(Type queryType, Type interfaceType) - { - Type? bestMatch = null; - var interfaces = queryType.GetInterfaces(); - foreach (var @interface in interfaces) - { - if (IsGenericInstantiation(@interface, interfaceType)) - { - if (bestMatch == null) - { - bestMatch = @interface; - } - else if (StringComparer.Ordinal.Compare(@interface.FullName, bestMatch.FullName) < 0) - { - bestMatch = @interface; - } - else - { - // There are two matches at this level of the class hierarchy, but @interface is after - // bestMatch in the sort order. - } - } - } - - if (bestMatch != null) - { - return bestMatch; - } - - // BaseType will be null for object and interfaces, which means we've reached 'bottom'. - var baseType = queryType?.BaseType; - if (baseType == null) - { - return null; - } - else - { - return GetGenericInstantiation(baseType, interfaceType); - } - } -} diff --git a/src/Components/Endpoints/src/Binding/DefaultFormBinderProvider.cs b/src/Components/Endpoints/src/Binding/DefaultFormBinderProvider.cs deleted file mode 100644 index f621ae7e43d3..000000000000 --- a/src/Components/Endpoints/src/Binding/DefaultFormBinderProvider.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Reflection; -using Microsoft.AspNetCore.Components.Binding; -using Microsoft.AspNetCore.Components.Forms; - -namespace Microsoft.AspNetCore.Components.Endpoints; - -internal class DefaultFormBinderProvider : IFormBinderProvider -{ - private readonly FormDataProvider _formData; - // Note: This won't be implemented this way. - private Dictionary _cache = new(); - - private delegate bool TryParseInvoker(string value, IFormatProvider formatProvider, out object result); - - public DefaultFormBinderProvider(FormDataProvider formData) - { - _formData = formData; - } - - public bool CanBind(string formName, Type valueType) - { - return _formData.IsFormDataAvailable && - string.Equals(formName, _formData.Name, StringComparison.Ordinal) && - valueType == typeof(string); - } - - public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) - { - if (!CanBind(formName, valueType)) - { - boundValue = null; - return false; - } - - if (!_formData.Entries.TryGetValue("value", out var rawValue) || rawValue.Count != 1) - { - boundValue = null; - return false; - } - - var valueAsString = rawValue.ToString(); - - if (valueType == typeof(string)) - { - boundValue = valueAsString; - return true; - } - - var iParsable = ClosedGenericMatcher.ExtractGenericInterface(typeof(IParsable<>), valueType); - if (iParsable != null) - { - var method = ResolveIParsableTryParse(iParsable, valueType); - var parameters = new object[3]; - parameters[0] = valueAsString; - parameters[1] = CultureInfo.CurrentCulture; - var result = method.Invoke(null, parameters); - boundValue = parameters[2]; - - return result != null && (bool)result; - } - boundValue = null; - return false; - } - - internal static MethodInfo ResolveIParsableTryParse(Type type, Type parsable) - { - var map = type.GetInterfaceMap(parsable); - for (var i = 0; i < map.TargetMethods.Length; i++) - { - var method = map.TargetMethods[i]; - var methodNameStart = method.Name.LastIndexOf('.') + 1; - if (method.Name.AsSpan()[methodNameStart..].Equals( - nameof(IParsable.TryParse), - StringComparison.Ordinal)) - { - var parameters = method.GetParameters(); - if (parameters.Length == 3 && - parameters[0].ParameterType == typeof(string) && - parameters[1].ParameterType == typeof(IFormatProvider) && - parameters[2].ParameterType == type.MakeByRefType()) - { - return method; - } - } - } - - throw new InvalidOperationException($"Unable to resolve TryParse(string s, IFormatProvider, out T result) for type '{type.FullName}'"); - } -} diff --git a/src/Components/Endpoints/src/Binding/DefaultFormValuesSupplier.cs b/src/Components/Endpoints/src/Binding/DefaultFormValuesSupplier.cs new file mode 100644 index 000000000000..187a0578680c --- /dev/null +++ b/src/Components/Endpoints/src/Binding/DefaultFormValuesSupplier.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; +using Microsoft.AspNetCore.Components.Forms; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal class DefaultFormValuesSupplier : IFormValueSupplier +{ + private readonly FormDataProvider _formData; + + public DefaultFormValuesSupplier(FormDataProvider formData) + { + _formData = formData; + } + + public bool CanBind(string formName, Type valueType) + { + return _formData.IsFormDataAvailable && + string.Equals(formName, _formData.Name, StringComparison.Ordinal) && + valueType == typeof(string); + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) + { + // This will delegate to a proper binder + if (!CanBind(formName, valueType)) + { + boundValue = null; + return false; + } + + if (!_formData.Entries.TryGetValue("value", out var rawValue) || rawValue.Count != 1) + { + boundValue = null; + return false; + } + + var valueAsString = rawValue.ToString(); + + if (valueType == typeof(string)) + { + boundValue = valueAsString; + return true; + } + + boundValue = null; + return false; + } +} diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 3de07248dfcb..8ef454f5bbf9 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -57,7 +57,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection // Form handling services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); return new DefaultRazorComponentsBuilder(services); } From 91429ebcaa8d44ea4973afa8da94d24c034ac26c Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 18:41:25 +0200 Subject: [PATCH 05/14] Fix tests --- .../test/AuthorizeRouteViewTest.cs | 17 ++++ .../test/CascadingModelBinderTest.cs | 94 ++++--------------- .../test/ParameterViewTest.Assignment.cs | 2 +- .../Components/test/RouteViewTest.cs | 19 +++- .../Samples/BlazorUnitedApp/Pages/Index.razor | 8 +- .../ExpressionFormatter.cs | 9 +- src/Components/Web/test/Forms/EditFormTest.cs | 17 ++++ 7 files changed, 81 insertions(+), 85 deletions(-) diff --git a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs index e0db0c46914d..c099b0c91063 100644 --- a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs +++ b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; @@ -32,6 +34,7 @@ public AuthorizeRouteViewTest() serviceCollection.AddSingleton(); serviceCollection.AddSingleton(_testAuthorizationService); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); @@ -467,4 +470,18 @@ public TestNavigationManager() Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash"); } } + + private class TestFormValueSupplier : IFormValueSupplier + { + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = null; + return false; + } + } } diff --git a/src/Components/Components/test/CascadingModelBinderTest.cs b/src/Components/Components/test/CascadingModelBinderTest.cs index dfb659231940..543770f3d59f 100644 --- a/src/Components/Components/test/CascadingModelBinderTest.cs +++ b/src/Components/Components/test/CascadingModelBinderTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.Extensions.DependencyInjection; @@ -18,6 +19,7 @@ public CascadingModelBinderTest() var serviceCollection = new ServiceCollection(); _navigationManager = new TestNavigationManager(); serviceCollection.AddSingleton(_navigationManager); + serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); } @@ -211,84 +213,6 @@ public void Throws_WhenIsFixedAndNameChanges() Assert.Equal("'CascadingModelBinder' 'Name' can't change after initialized.", exception.Message); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Throws_WhenIsFixed_Changes(bool isFixed) - { - ModelBindingContext capturedContext = null; - RenderFragment contents = (ctx) => b => { capturedContext = ctx; }; - var testComponent = new TestComponent(builder => - { - builder.OpenComponent(0); - builder.AddAttribute(2, nameof(CascadingModelBinder.ChildContent), contents); - builder.CloseComponent(); - }); - var id = _renderer.AssignRootComponentId(testComponent); - _renderer.RenderRootComponent(id); - - // Act - isFixed = !isFixed; - var exception = Assert.Throws(testComponent.TriggerRender); - - Assert.Equal("The value of IsFixed cannot be changed dynamically.", exception.Message); - } - - [Fact] - public void CanChange_Name_WhenNotFixed() - { - ModelBindingContext capturedContext = null; - ModelBindingContext originalContext = null; - RenderFragment contents = (ctx) => b => { capturedContext = ctx; }; - var contextName = "parent-context"; - - var testComponent = new TestComponent(builder => - { - builder.OpenComponent(0); - builder.AddAttribute(1, nameof(CascadingModelBinder.Name), contextName); - builder.AddAttribute(3, nameof(CascadingModelBinder.ChildContent), contents); - builder.CloseComponent(); - }); - var id = _renderer.AssignRootComponentId(testComponent); - _renderer.RenderRootComponent(id); - - originalContext = capturedContext; - contextName = "changed"; - - // Act - testComponent.TriggerRender(); - - Assert.NotSame(capturedContext, originalContext); - Assert.Equal("changed", capturedContext.Name); - } - - [Fact] - public void CanChange_BindingContextId_WhenNotFixed() - { - ModelBindingContext capturedContext = null; - ModelBindingContext originalContext = null; - RenderFragment contents = (ctx) => b => { capturedContext = ctx; }; - - var testComponent = new TestComponent(builder => - { - builder.OpenComponent(0); - builder.AddComponentParameter(1, nameof(CascadingModelBinder.Name), "context-name"); - builder.AddComponentParameter(2, nameof(CascadingModelBinder.ChildContent), contents); - builder.CloseComponent(); - }); - var id = _renderer.AssignRootComponentId(testComponent); - _renderer.RenderRootComponent(id); - - originalContext = capturedContext; - - // Act - _navigationManager.NavigateTo(_navigationManager.ToAbsoluteUri("fetch-data/6").ToString()); - testComponent.TriggerRender(); - - Assert.NotSame(capturedContext, originalContext); - Assert.Equal("fetch-data/6?handler=context-name", capturedContext.BindingContextId); - } - private class RouteViewTestNavigationManager : NavigationManager { public RouteViewTestNavigationManager() => @@ -326,4 +250,18 @@ public TestComponent(RenderFragment renderFragment) protected override void BuildRenderTree(RenderTreeBuilder builder) => _renderFragment(builder); } + + private class TestFormValueSupplier : IFormValueSupplier + { + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = null; + return false; + } + } } diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 8fff5c2003d0..bcc032c7e080 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -181,7 +181,7 @@ public void IncomingParameterMatchesPropertyNotDeclaredAsParameter_Throws() Assert.Equal(default, target.IntProp); Assert.Equal( $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " + - $"but it does not have [{nameof(ParameterAttribute)}] or [{nameof(CascadingParameterAttribute)}] applied.", + $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or [{nameof(SupplyParameterFromFormAttribute)}] applied.", ex.Message); } diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs index 92abd7e97b47..6915d01c9b24 100644 --- a/src/Components/Components/test/RouteViewTest.cs +++ b/src/Components/Components/test/RouteViewTest.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.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.Extensions.DependencyInjection; @@ -19,6 +21,7 @@ public RouteViewTest() var serviceCollection = new ServiceCollection(); _navigationManager = new RouteViewTestNavigationManager(); serviceCollection.AddSingleton(_navigationManager); + serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); @@ -86,7 +89,7 @@ public void RendersPageInsideLayoutView() var cascadingModelBinderFrames = _renderer.GetCurrentRenderTreeFrames(cascadingModelBinderComponentId).AsEnumerable(); Assert.Collection(cascadingModelBinderFrames, frame => AssertFrame.Component>(frame, sequence: 0, subtreeLength: 4), - frame => AssertFrame.Attribute(frame, nameof(CascadingValue.IsFixed), false, sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(CascadingValue.IsFixed), true, sequence: 1), frame => AssertFrame.Attribute(frame, nameof(CascadingValue.Value), typeof(ModelBindingContext), sequence: 2), frame => AssertFrame.Attribute(frame, nameof(CascadingValue.ChildContent), typeof(RenderFragment), sequence: 3)); @@ -237,4 +240,18 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddContent(2, "OtherLayout ends here"); } } + + private class TestFormValueSupplier : IFormValueSupplier + { + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = null; + return false; + } + } } diff --git a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor index d8e88701afff..bb031aa6572f 100644 --- a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor +++ b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor @@ -2,10 +2,10 @@ Index -

@value

+

@Parameter

- - + + @@ -15,7 +15,7 @@ } @code{ - [SupplyParameterFromForm] string value { get; set; } = "Hello, world!"; + [SupplyParameterFromForm] string Parameter { get; set; } = "Hello, world!"; bool _submitted = false; public void Submit() diff --git a/src/Components/Web/src/Forms/ExpressionFormatting/ExpressionFormatter.cs b/src/Components/Web/src/Forms/ExpressionFormatting/ExpressionFormatter.cs index e3fc1b199616..ea1b8cfa7b6c 100644 --- a/src/Components/Web/src/Forms/ExpressionFormatting/ExpressionFormatter.cs +++ b/src/Components/Web/src/Forms/ExpressionFormatting/ExpressionFormatter.cs @@ -74,6 +74,13 @@ public static string FormatLambda(LambdaExpression expression) if (nextNode?.NodeType == ExpressionType.Constant) { + // Special case primitive values that are bound directly from the form. + // By convention, the name for the field will be "value". + if (memberExpression.Member.IsDefined(typeof(SupplyParameterFromFormAttribute), inherit: false) + && memberExpression.Type == typeof(string)) + { + builder.InsertFront("value"); + } // The next node has a compiler-generated closure type, // which means the current member access is on the captured model. // We don't want to include the model variable name in the generated @@ -165,7 +172,7 @@ static MethodInfoData GetMethodInfoData(MethodInfo methodInfo) return new(IsSingleArgumentIndexer: false); } } - + private static void FormatIndexArgument( Expression indexExpression, ref ReverseStringBuilder builder) diff --git a/src/Components/Web/test/Forms/EditFormTest.cs b/src/Components/Web/test/Forms/EditFormTest.cs index ae08af1ccdf9..fa460ec92061 100644 --- a/src/Components/Web/test/Forms/EditFormTest.cs +++ b/src/Components/Web/test/Forms/EditFormTest.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; @@ -17,6 +19,7 @@ public EditFormTest() { var services = new ServiceCollection(); services.AddSingleton(); + services.AddSingleton(); _testRenderer = new(services.BuildServiceProvider()); } @@ -440,4 +443,18 @@ public TestNavigationManager() Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash"); } } + + private class TestFormValueSupplier : IFormValueSupplier + { + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = null; + return false; + } + } } From 454a2fa4225263ffd6d25f66ba365bfe98ef9c23 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 19:04:11 +0200 Subject: [PATCH 06/14] Cleanups --- .../Components/src/SupplyParameterFromFromAttribute.cs | 9 ++++----- .../src/Microsoft.AspNetCore.Components.Server.csproj | 7 ++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Components/Components/src/SupplyParameterFromFromAttribute.cs b/src/Components/Components/src/SupplyParameterFromFromAttribute.cs index 453c8f1026df..b2f5d4ef7545 100644 --- a/src/Components/Components/src/SupplyParameterFromFromAttribute.cs +++ b/src/Components/Components/src/SupplyParameterFromFromAttribute.cs @@ -4,15 +4,14 @@ namespace Microsoft.AspNetCore.Components; /// -/// Indicates that routing components may supply a value for the parameter from the -/// current URL querystring. They may also supply further values if the URL querystring changes. -/// +/// Indicates that the value of the associated property should be supplied from +/// the form data for the form with the specified name. [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class SupplyParameterFromFormAttribute : Attribute { /// - /// Gets or sets the name of the querystring parameter. If null, the querystring - /// parameter is assumed to have the same name as the associated property. + /// Gets or sets the name for the parameter. The name is used to match + /// the form data and decide whether or not the value needs to be bound. /// public string? Name { get; set; } } diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index c6ac0f082ce5..942fc3b663e2 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -37,7 +37,12 @@ - + From 6e7c52ca93d59a22f8ddb61e7f761b29879586e9 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 19:08:49 +0200 Subject: [PATCH 07/14] Fix XML doc comment --- .../Components/src/SupplyParameterFromFromAttribute.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Components/Components/src/SupplyParameterFromFromAttribute.cs b/src/Components/Components/src/SupplyParameterFromFromAttribute.cs index b2f5d4ef7545..f643833a3f79 100644 --- a/src/Components/Components/src/SupplyParameterFromFromAttribute.cs +++ b/src/Components/Components/src/SupplyParameterFromFromAttribute.cs @@ -6,6 +6,7 @@ namespace Microsoft.AspNetCore.Components; /// /// Indicates that the value of the associated property should be supplied from /// the form data for the form with the specified name. +/// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class SupplyParameterFromFormAttribute : Attribute { From 3e2e0bf3b57b0fd9dbb3971003adf66e9a625a18 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 19:54:22 +0200 Subject: [PATCH 08/14] Cleanups and tests --- .../src/Binding/CascadingModelBinder.cs | 6 +- ...cs => SupplyParameterFromFormAttribute.cs} | 0 .../test/CascadingParameterStateTest.cs | 106 ++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) rename src/Components/Components/src/{SupplyParameterFromFromAttribute.cs => SupplyParameterFromFormAttribute.cs} (100%) diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index d863d4c5da4d..20e0ffc2dd92 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -30,9 +30,9 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, [CascadingParameter] ModelBindingContext? ParentContext { get; set; } - [Inject] private NavigationManager Navigation { get; set; } = null!; + [Inject] internal NavigationManager Navigation { get; set; } = null!; - [Inject] private IFormValueSupplier FormValueSupplier { get; set; } = null!; + [Inject] internal IFormValueSupplier FormValueSupplier { get; set; } = null!; void IComponent.Attach(RenderHandle renderHandle) { @@ -84,7 +84,7 @@ private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) Render(); } - private void UpdateBindingInformation(string url) + internal void UpdateBindingInformation(string url) { // BindingContextId: action parameter used to define the handler // Name: form name and context used to bind diff --git a/src/Components/Components/src/SupplyParameterFromFromAttribute.cs b/src/Components/Components/src/SupplyParameterFromFormAttribute.cs similarity index 100% rename from src/Components/Components/src/SupplyParameterFromFromAttribute.cs rename to src/Components/Components/src/SupplyParameterFromFormAttribute.cs diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index 648056c152c0..1d490d70fcfe 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; +using Moq; namespace Microsoft.AspNetCore.Components; @@ -355,6 +358,68 @@ public void FindCascadingParameters_CanOverrideNonNullValueWithNull() }); } + [Fact] + public void FindCascadingParameters_HandlesSupplyParameterFromFormValues() + { + // Arrange + var cascadingModelBinder = new CascadingModelBinder + { + FormValueSupplier = new TestFormValueSupplier() + { + FormName = "", + ValueType = typeof(string), + BindResult = true, + BoundValue = "some value" + }, + Navigation = Mock.Of(), + Name = "" + }; + + cascadingModelBinder.UpdateBindingInformation("https://localhost/"); + + var states = CreateAncestry( + cascadingModelBinder, + new FormParametersComponent()); + + // Act + var result = CascadingParameterState.FindCascadingParameters(states.Last()); + + // Assert + var supplier = Assert.Single(result); + Assert.Equal(cascadingModelBinder, supplier.ValueSupplier); + } + + [Fact] + public void FindCascadingParameters_HandlesSupplyParameterFromFormValues_WithName() + { + // Arrange + var cascadingModelBinder = new CascadingModelBinder + { + FormValueSupplier = new TestFormValueSupplier() + { + FormName = "some-name", + ValueType = typeof(string), + BindResult = true, + BoundValue = "some value" + }, + Navigation = new TestNavigationManager(), + Name = "" + }; + + cascadingModelBinder.UpdateBindingInformation("https://localhost/"); + + var states = CreateAncestry( + cascadingModelBinder, + new FormParametersComponentWithName()); + + // Act + var result = CascadingParameterState.FindCascadingParameters(states.Last()); + + // Assert + var supplier = Assert.Single(result); + Assert.Equal(cascadingModelBinder, supplier.ValueSupplier); + } + static ComponentState[] CreateAncestry(params IComponent[] components) { var result = new ComponentState[components.Length]; @@ -399,6 +464,16 @@ class ComponentWithNoParams : TestComponentBase { } + class FormParametersComponent : TestComponentBase + { + [SupplyParameterFromForm] public string FormParameter { get; set; } + } + + class FormParametersComponentWithName : TestComponentBase + { + [SupplyParameterFromForm(Name = "some-name")] public string FormParameter { get; set; } + } + class ComponentWithNoCascadingParams : TestComponentBase { [Parameter] public bool SomeRegularParameter { get; set; } @@ -443,4 +518,35 @@ class ValueType3 { } class CascadingValueTypeBaseClass { } class CascadingValueTypeDerivedClass : CascadingValueTypeBaseClass, ICascadingValueTypeDerivedClassInterface { } interface ICascadingValueTypeDerivedClassInterface { } + + private class TestFormValueSupplier : IFormValueSupplier + { + public string FormName { get; set; } + + public Type ValueType { get; set; } + + public object BoundValue { get; set; } + + public bool BindResult { get; set; } + + public bool CanBind(string formName, Type valueType) + { + return string.Equals(formName, FormName, StringComparison.Ordinal) && + valueType == ValueType; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = BoundValue; + return BindResult; + } + } + + class TestNavigationManager : NavigationManager + { + public TestNavigationManager() + { + Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash"); + } + } } From dbbee364bafe9940d88a1ae128ae4ba5b867574b Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 21:49:03 +0200 Subject: [PATCH 09/14] Add E2E tests --- .../src/Binding/CascadingModelBinder.cs | 2 +- .../FormWithParentBindingContextTest.cs | 65 ++++++++++++++++--- .../ComponentWithFormBoundParameter.razor | 17 +++++ ....razor => FormWithDefaultContextApp.razor} | 2 +- .../Forms/DefaultFormBoundParameter.razor | 20 ++++++ .../Pages/Forms/NamedFormBoundParameter.razor | 20 ++++++ .../Forms/NestedNamedFormBoundParameter.razor | 8 +++ 7 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor rename src/Components/test/testassets/Components.TestServer/RazorComponents/{FormWithoutBindingContextApp.razor => FormWithDefaultContextApp.razor} (91%) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameter.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NamedFormBoundParameter.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NestedNamedFormBoundParameter.razor diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index 20e0ffc2dd92..d1c3bc14a4ec 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -104,7 +104,7 @@ internal void UpdateBindingInformation(string url) var bindingId = string.IsNullOrEmpty(name) ? "" : GenerateBindingContextId(name); var bindingContext = _bindingContext != null && - string.Equals(_bindingContext.Name, Name, StringComparison.Ordinal) && + string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) && string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal) ? _bindingContext : new ModelBindingContext(name, bindingId); diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs index 928774c49e10..d0799f139fd4 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs @@ -1,23 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using System.Net.Http; +using Components.TestServer.RazorComponents; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; using TestServer; using Xunit.Abstractions; -using OpenQA.Selenium; -using System.Net.Http; -using static System.Net.Mime.MediaTypeNames; -using Components.TestServer.RazorComponents; namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.FormHandlingTests; -public class FormWithParentBindingContextTest : ServerTestBase>> +public class FormWithParentBindingContextTest : ServerTestBase>> { public FormWithParentBindingContextTest( BrowserFixture browserFixture, - BasicTestAppServerSiteFixture> serverFixture, + BasicTestAppServerSiteFixture> serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { @@ -51,6 +50,21 @@ public void CanDispatchToTheDefaultFormWithBody() DispatchToFormCore(dispatchToForm); } + [Fact] + public void CanBindParameterToTheDefaultForm() + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/default-form-bound-parameter", + FormCssSelector = "form", + ExpectedActionValue = null, + InputFieldId = "value", + InputFieldCssSelector = "input[name=value]", + InputFieldValue = "stranger", + }; + DispatchToFormCore(dispatchToForm); + } + [Fact] public void CanReadFormValuesDuringOnInitialized() { @@ -76,6 +90,21 @@ public void CanDispatchToNamedForm() DispatchToFormCore(dispatchToForm); } + [Fact] + public void CanBindFormValueFromNamedFormWithBody() + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/named-form-bound-parameter", + FormCssSelector = "form[name=named-form-handler]", + ExpectedActionValue = "forms/named-form-bound-parameter?handler=named-form-handler", + InputFieldId = "value", + InputFieldCssSelector = "input[name=value]", + InputFieldValue = "stranger", + }; + DispatchToFormCore(dispatchToForm); + } + [Fact] public void CanDispatchToNamedFormInNestedContext() { @@ -88,6 +117,21 @@ public void CanDispatchToNamedFormInNestedContext() DispatchToFormCore(dispatchToForm); } + [Fact] + public void CanBindFormValueFromNestedNamedFormWithBody() + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/nested-named-form-bound-parameter", + FormCssSelector = """form[name="parent-context.named-form-handler"]""", + ExpectedActionValue = "forms/nested-named-form-bound-parameter?handler=parent-context.named-form-handler", + InputFieldId = "value", + InputFieldCssSelector = "input[name=value]", + InputFieldValue = "stranger", + }; + DispatchToFormCore(dispatchToForm); + } + [Fact] public void CanDispatchToFormDefinedInNonPageComponent() { @@ -251,7 +295,11 @@ private void DispatchToFormCore(DispatchToForm dispatch) if (dispatch.InputFieldValue != null) { - Browser.Exists(By.Id(dispatch.InputFieldId)).SendKeys(dispatch.InputFieldValue); + var criteria = dispatch.InputFieldCssSelector != null ? + By.CssSelector(dispatch.InputFieldCssSelector) : + By.Id(dispatch.InputFieldId); + + Browser.Exists(criteria).SendKeys(dispatch.InputFieldValue); } Browser.Click(By.Id(dispatch.SubmitButtonId)); @@ -284,6 +332,7 @@ public DispatchToForm(FormWithParentBindingContextTest test) : this() public string SubmitButtonId { get; internal set; } = "send"; public string InputFieldId { get; internal set; } = "firstName"; + public string InputFieldCssSelector { get; internal set; } = null; } private void GoTo(string relativePath) diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor new file mode 100644 index 000000000000..1b55105e58b4 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor @@ -0,0 +1,17 @@ +@using Microsoft.AspNetCore.Components.Forms + + + + + + +@if (_submitted) +{ +

Hello @Parameter!

+} + +@code{ + bool _submitted = false; + + [SupplyParameterFromForm(Name = "named-form-handler")] public string Parameter { get; set; } = ""; +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithoutBindingContextApp.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithDefaultContextApp.razor similarity index 91% rename from src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithoutBindingContextApp.razor rename to src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithDefaultContextApp.razor index ec4501fa7209..ab400f1f3543 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithoutBindingContextApp.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithDefaultContextApp.razor @@ -9,7 +9,7 @@ - + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameter.razor new file mode 100644 index 000000000000..0cda2a205275 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameter.razor @@ -0,0 +1,20 @@ +@page "/forms/default-form-bound-parameter" +@using Microsoft.AspNetCore.Components.Forms + +

Default form with bound parameter

+ + + + + + +@if (_submitted) +{ +

Hello @Parameter!

+} + +@code { + bool _submitted = false; + + [SupplyParameterFromForm] public string Parameter { get; set; } = ""; +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NamedFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NamedFormBoundParameter.razor new file mode 100644 index 000000000000..55d1adf585fa --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NamedFormBoundParameter.razor @@ -0,0 +1,20 @@ +@page "/forms/named-form-bound-parameter" +@using Microsoft.AspNetCore.Components.Forms + +

Named form with bound parameter

+ + + + + + +@if (_submitted) +{ +

Hello @Parameter!

+} + +@code{ + bool _submitted = false; + + [SupplyParameterFromForm(Name = "named-form-handler")] public string Parameter { get; set; } = ""; +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NestedNamedFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NestedNamedFormBoundParameter.razor new file mode 100644 index 000000000000..29750b239c0d --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NestedNamedFormBoundParameter.razor @@ -0,0 +1,8 @@ +@page "/forms/nested-named-form-bound-parameter" +@using Microsoft.AspNetCore.Components.Forms + +

Nested named form bound parameter

+ + + + From d04febe5e88d2b8fa18389b603bf34a30def3f09 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 31 May 2023 12:04:16 +0200 Subject: [PATCH 10/14] Undo the IsFixed change --- .../src/Binding/CascadingModelBinder.cs | 12 ++- .../Components/src/PublicAPI.Unshipped.txt | 2 + .../test/CascadingModelBinderTest.cs | 80 +++++++++++++++++++ .../Components/test/RouteViewTest.cs | 2 +- 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index d1c3bc14a4ec..6743ef4db95a 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -23,6 +23,14 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, ///
[Parameter] public string Name { get; set; } = ""; + /// + /// If true, indicates that will not change. + /// This is a performance optimization that allows the framework to skip setting up + /// change notifications. Set this flag only if you will not change + /// of this context or its parents' context during the component's lifetime. + /// + [Parameter] public bool IsFixed { get; set; } + /// /// Specifies the content to be rendered inside this . /// @@ -70,7 +78,7 @@ private void Render() { _hasPendingQueuedRender = false; builder.OpenComponent>(0); - builder.AddComponentParameter(1, nameof(CascadingValue.IsFixed), true); + builder.AddComponentParameter(1, nameof(CascadingValue.IsFixed), IsFixed); builder.AddComponentParameter(2, nameof(CascadingValue.Value), _bindingContext); builder.AddComponentParameter(3, nameof(CascadingValue.ChildContent), ChildContent?.Invoke(_bindingContext!)); builder.CloseComponent(); @@ -109,7 +117,7 @@ internal void UpdateBindingInformation(string url) _bindingContext : new ModelBindingContext(name, bindingId); // It doesn't matter that we don't check IsFixed, since the CascadingValue we are setting up will throw if the app changes. - if (_bindingContext != null && _bindingContext != bindingContext) + if (IsFixed && _bindingContext != null && _bindingContext != bindingContext) { // Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized // as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations: diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index eb1dc00c331f..0b47fa7b6a13 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -7,6 +7,8 @@ Microsoft.AspNetCore.Components.CascadingModelBinder Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment! Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.set -> void +Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.get -> bool +Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.set -> void Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string! Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! diff --git a/src/Components/Components/test/CascadingModelBinderTest.cs b/src/Components/Components/test/CascadingModelBinderTest.cs index 543770f3d59f..5bdd96417b86 100644 --- a/src/Components/Components/test/CascadingModelBinderTest.cs +++ b/src/Components/Components/test/CascadingModelBinderTest.cs @@ -200,6 +200,7 @@ public void Throws_WhenIsFixedAndNameChanges() { builder.OpenComponent(0); builder.AddAttribute(1, nameof(CascadingModelBinder.Name), contextName); + builder.AddAttribute(2, nameof(CascadingModelBinder.IsFixed), true); builder.AddAttribute(3, nameof(CascadingModelBinder.ChildContent), contents); builder.CloseComponent(); }); @@ -213,6 +214,85 @@ public void Throws_WhenIsFixedAndNameChanges() Assert.Equal("'CascadingModelBinder' 'Name' can't change after initialized.", exception.Message); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Throws_WhenIsFixed_Changes(bool isFixed) + { + ModelBindingContext capturedContext = null; + RenderFragment contents = (ctx) => b => { capturedContext = ctx; }; + var testComponent = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(CascadingModelBinder.IsFixed), isFixed); + builder.AddAttribute(2, nameof(CascadingModelBinder.ChildContent), contents); + builder.CloseComponent(); + }); + var id = _renderer.AssignRootComponentId(testComponent); + _renderer.RenderRootComponent(id); + + // Act + isFixed = !isFixed; + var exception = Assert.Throws(testComponent.TriggerRender); + + Assert.Equal("The value of IsFixed cannot be changed dynamically.", exception.Message); + } + + [Fact] + public void CanChange_Name_WhenNotFixed() + { + ModelBindingContext capturedContext = null; + ModelBindingContext originalContext = null; + RenderFragment contents = (ctx) => b => { capturedContext = ctx; }; + var contextName = "parent-context"; + + var testComponent = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(CascadingModelBinder.Name), contextName); + builder.AddAttribute(3, nameof(CascadingModelBinder.ChildContent), contents); + builder.CloseComponent(); + }); + var id = _renderer.AssignRootComponentId(testComponent); + _renderer.RenderRootComponent(id); + + originalContext = capturedContext; + contextName = "changed"; + + // Act + testComponent.TriggerRender(); + + Assert.NotSame(capturedContext, originalContext); + Assert.Equal("changed", capturedContext.Name); + } + + [Fact] + public void CanChange_BindingContextId_WhenNotFixed() + { + ModelBindingContext capturedContext = null; + ModelBindingContext originalContext = null; + RenderFragment contents = (ctx) => b => { capturedContext = ctx; }; + + var testComponent = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.AddComponentParameter(1, nameof(CascadingModelBinder.Name), "context-name"); + builder.AddComponentParameter(2, nameof(CascadingModelBinder.ChildContent), contents); + builder.CloseComponent(); + }); + var id = _renderer.AssignRootComponentId(testComponent); + _renderer.RenderRootComponent(id); + + originalContext = capturedContext; + + // Act + _navigationManager.NavigateTo(_navigationManager.ToAbsoluteUri("fetch-data/6").ToString()); + testComponent.TriggerRender(); + + Assert.NotSame(capturedContext, originalContext); + Assert.Equal("fetch-data/6?handler=context-name", capturedContext.BindingContextId); + } + private class RouteViewTestNavigationManager : NavigationManager { public RouteViewTestNavigationManager() => diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs index 6915d01c9b24..fbec4e51b979 100644 --- a/src/Components/Components/test/RouteViewTest.cs +++ b/src/Components/Components/test/RouteViewTest.cs @@ -89,7 +89,7 @@ public void RendersPageInsideLayoutView() var cascadingModelBinderFrames = _renderer.GetCurrentRenderTreeFrames(cascadingModelBinderComponentId).AsEnumerable(); Assert.Collection(cascadingModelBinderFrames, frame => AssertFrame.Component>(frame, sequence: 0, subtreeLength: 4), - frame => AssertFrame.Attribute(frame, nameof(CascadingValue.IsFixed), true, sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(CascadingValue.IsFixed), false, sequence: 1), frame => AssertFrame.Attribute(frame, nameof(CascadingValue.Value), typeof(ModelBindingContext), sequence: 2), frame => AssertFrame.Attribute(frame, nameof(CascadingValue.ChildContent), typeof(RenderFragment), sequence: 3)); From 91abddaf104a0969fd0056925dbd0d1604bd8a6d Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 26 May 2023 18:05:40 +0200 Subject: [PATCH 11/14] Move SupplyParameterFromFormAttribute to Microsoft.AspNetCore.Components.Web --- .../Components/src/CascadingParameterState.cs | 8 +++-- .../src/IHostEnvironmentCascadingParameter.cs | 11 ++++++ .../Components/src/PublicAPI.Unshipped.txt | 4 --- .../src/Reflection/ComponentProperties.cs | 34 +++++++++++++++---- .../test/CascadingParameterStateTest.cs | 10 ++++++ .../Web/src/PublicAPI.Unshipped.txt | 4 +++ .../src/SupplyParameterFromFormAttribute.cs | 2 +- 7 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 src/Components/Components/src/IHostEnvironmentCascadingParameter.cs rename src/Components/{Components => Web}/src/SupplyParameterFromFormAttribute.cs (95%) diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index e392cd4ac80d..7af4dc1b9cce 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Components.Reflection; using Microsoft.AspNetCore.Components.Rendering; @@ -103,15 +104,16 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet attribute.Name)); } - var fromForm = prop.GetCustomAttribute(); - if (fromForm != null) + var hostParameterAttribute = prop.GetCustomAttributes() + .OfType().SingleOrDefault(); + if (hostParameterAttribute != null) { result ??= new List(); result.Add(new ReflectedCascadingParameterInfo( prop.Name, prop.PropertyType, - fromForm.Name)); + hostParameterAttribute.Name)); } } diff --git a/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs b/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs new file mode 100644 index 000000000000..8f407e0cdd5e --- /dev/null +++ b/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +// Marks a cascading parameter that can be offered via an attribute that is not +// directly defined in the Components assembly. For example [SupplyParameterFromForm]. +internal interface IHostEnvironmentCascadingParameter +{ + public string? Name { get; set; } +} diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 0b47fa7b6a13..7fe8a6a3519c 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -59,10 +59,6 @@ Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParamete Microsoft.AspNetCore.Components.StreamRenderingAttribute Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void -Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute -Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string? -Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void -Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.SupplyParameterFromFormAttribute() -> void override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index e7e69ff09927..9569e8e6a84e 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -170,12 +171,12 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem { if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && !propertyInfo.IsDefined(typeof(CascadingParameterAttribute)) && - !propertyInfo.IsDefined(typeof(SupplyParameterFromFormAttribute))) + !propertyInfo.GetCustomAttributes().OfType().Any()) { throw new InvalidOperationException( $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or " + - $"[{nameof(SupplyParameterFromFormAttribute)}] applied."); + $"[SupplyParameterFromFormAttribute] applied."); } else { @@ -260,11 +261,30 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) foreach (var propertyInfo in GetCandidateBindableProperties(targetType)) { - var parameterAttribute = propertyInfo.GetCustomAttribute(); - var cascadingParameterAttribute = propertyInfo.GetCustomAttribute(); - var supplyParameterFromFormAttribute = propertyInfo.GetCustomAttribute(); + ParameterAttribute? parameterAttribute = null; + CascadingParameterAttribute? cascadingParameterAttribute = null; + IHostEnvironmentCascadingParameter? hostEnvironmentCascadingParameter = null; - var isParameter = parameterAttribute != null || cascadingParameterAttribute != null || supplyParameterFromFormAttribute != null; + var attributes = propertyInfo.GetCustomAttributes(); + foreach (var attribute in attributes) + { + switch (attribute) + { + case ParameterAttribute parameter: + parameterAttribute = parameter; + break; + case CascadingParameterAttribute cascadingParameter: + cascadingParameterAttribute = cascadingParameter; + break; + case IHostEnvironmentCascadingParameter hostEnvironmentAttribute: + hostEnvironmentCascadingParameter = hostEnvironmentAttribute; + break; + default: + break; + } + } + + var isParameter = parameterAttribute != null || cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null; if (!isParameter) { continue; @@ -279,7 +299,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) var propertySetter = new PropertySetter(targetType, propertyInfo) { - Cascading = cascadingParameterAttribute != null || supplyParameterFromFormAttribute != null, + Cascading = cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null, }; if (_underlyingWriters.ContainsKey(propertyName)) diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index 1d490d70fcfe..95f1201137db 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -550,3 +550,13 @@ public TestNavigationManager() } } } + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class SupplyParameterFromFormAttribute : Attribute, IHostEnvironmentCascadingParameter +{ + /// + /// Gets or sets the name for the parameter. The name is used to match + /// the form data and decide whether or not the value needs to be bound. + /// + public string Name { get; set; } +} diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index dbeb2dc911e7..780363a5e104 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -17,6 +17,10 @@ Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer. Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(System.Type! componentType, Microsoft.AspNetCore.Components.ParameterView initialParameters) -> Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.StaticHtmlRenderer(System.IServiceProvider! serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Components.RenderTree.WebRenderer.WaitUntilAttachedAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string? +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.SupplyParameterFromFormAttribute() -> void Microsoft.AspNetCore.Components.Web.AutoRenderMode Microsoft.AspNetCore.Components.Web.AutoRenderMode.AutoRenderMode() -> void Microsoft.AspNetCore.Components.Web.AutoRenderMode.AutoRenderMode(bool prerender) -> void diff --git a/src/Components/Components/src/SupplyParameterFromFormAttribute.cs b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs similarity index 95% rename from src/Components/Components/src/SupplyParameterFromFormAttribute.cs rename to src/Components/Web/src/SupplyParameterFromFormAttribute.cs index f643833a3f79..2ae657c95583 100644 --- a/src/Components/Components/src/SupplyParameterFromFormAttribute.cs +++ b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// the form data for the form with the specified name. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromFormAttribute : Attribute +public sealed class SupplyParameterFromFormAttribute : Attribute, IHostEnvironmentCascadingParameter { /// /// Gets or sets the name for the parameter. The name is used to match From 3ff60a4e29a0d206f92b91f43dbcb116a22569bf Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 24 May 2023 23:58:35 +0200 Subject: [PATCH 12/14] Register default implementations on other platforms --- .../src/Hosting/WebAssemblyHostBuilder.cs | 1 + .../Services/WebAssemblyFormValueSupplier.cs | 20 ++++++++++++++++++ ...nentsWebViewServiceCollectionExtensions.cs | 2 ++ .../src/Services/WebViewFormValueSupplier.cs | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyFormValueSupplier.cs create mode 100644 src/Components/WebView/WebView/src/Services/WebViewFormValueSupplier.cs diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index f5d525cbdaf0..3345d113eb4b 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -262,5 +262,6 @@ internal void InitializeDefaultServices() builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance)); }); Services.AddSingleton(); + Services.AddSingleton(); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyFormValueSupplier.cs b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyFormValueSupplier.cs new file mode 100644 index 000000000000..bb88521ed323 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyFormValueSupplier.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Services; +internal class WebAssemblyFormValueSupplier : IFormValueSupplier +{ + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) + { + boundValue = null; + return false; + } +} diff --git a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs index e6420673fbbf..e522547c6971 100644 --- a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs +++ b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs @@ -31,6 +31,8 @@ public static IServiceCollection AddBlazorWebView(this IServiceCollection servic services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); + return services; } } diff --git a/src/Components/WebView/WebView/src/Services/WebViewFormValueSupplier.cs b/src/Components/WebView/WebView/src/Services/WebViewFormValueSupplier.cs new file mode 100644 index 000000000000..39af8f25e3f5 --- /dev/null +++ b/src/Components/WebView/WebView/src/Services/WebViewFormValueSupplier.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; + +namespace Microsoft.AspNetCore.Components.WebView.Services; + +internal class WebViewFormValueSupplier : IFormValueSupplier +{ + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) + { + boundValue = null; + return false; + } +} From 33089690ed2f3c131fef603387caca8be9ec6844 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 26 May 2023 11:27:53 +0200 Subject: [PATCH 13/14] Update src/Components/Components/src/Binding/IFormValueSupplier.cs Co-authored-by: Mackinnon Buck --- src/Components/Components/src/Binding/IFormValueSupplier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/Binding/IFormValueSupplier.cs b/src/Components/Components/src/Binding/IFormValueSupplier.cs index 90ddd6a86650..364f4041b362 100644 --- a/src/Components/Components/src/Binding/IFormValueSupplier.cs +++ b/src/Components/Components/src/Binding/IFormValueSupplier.cs @@ -15,7 +15,7 @@ public interface IFormValueSupplier /// /// The form name to bind data from. /// The for the value to bind. - /// + /// true if the value type can be bound; otherwise, false. bool CanBind(string formName, Type valueType); /// From b1c47e3c485e49dd78ee22c695058a9139273045 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 26 May 2023 12:17:11 +0200 Subject: [PATCH 14/14] Address feedback --- .../src/Binding/CascadingModelBinder.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index 6743ef4db95a..cdf6d4d2e755 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -162,12 +162,7 @@ bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName) if (FormValueSupplier.CanBind(formName!, valueType)) { var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue); - _bindingInfo = new BindingInfo(formName, valueType) - { - BindingResult = bindingSucceeded, - BoundValue = boundValue, - }; - + _bindingInfo = new BindingInfo(formName, valueType, bindingSucceeded, boundValue); if (!bindingSucceeded) { // Report errors @@ -175,7 +170,7 @@ bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName) return true; } - + return false; } @@ -195,10 +190,5 @@ void ICascadingValueComponent.Unsubscribe(ComponentState subscriber) bool ICascadingValueComponent.CurrentValueIsFixed => true; - private record BindingInfo(string? FormName, Type ValueType) - { - public required bool BindingResult { get; set; } - - public required object? BoundValue { get; set; } - } + private record BindingInfo(string? FormName, Type ValueType, bool BindingResult, object? BoundValue); }