Skip to content

Commit 2d9d92e

Browse files
Make specifying the form name on [SupplyParameterFromForm] optional. Still needs more test updates.
1 parent fcbeeb3 commit 2d9d92e

File tree

12 files changed

+108
-56
lines changed

12 files changed

+108
-56
lines changed

src/Components/Endpoints/src/FormMapping/HttpContextFormDataProvider.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,29 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.ObjectModel;
5+
using System.Diagnostics.CodeAnalysis;
56
using Microsoft.Extensions.Primitives;
67

78
namespace Microsoft.AspNetCore.Components.Endpoints;
89

910
internal sealed class HttpContextFormDataProvider
1011
{
11-
private string? _name;
12+
private string? _incomingHandlerName;
1213
private IReadOnlyDictionary<string, StringValues>? _entries;
1314

14-
public string? Name => _name;
15+
public string? IncomingHandlerName => _incomingHandlerName;
1516

1617
public IReadOnlyDictionary<string, StringValues> Entries => _entries ?? ReadOnlyDictionary<string, StringValues>.Empty;
1718

18-
public bool IsFormDataAvailable => Name != null;
19-
2019
public void SetFormData(string name, IReadOnlyDictionary<string, StringValues> form)
2120
{
22-
_name = name;
21+
_incomingHandlerName = name;
2322
_entries = form;
2423
}
24+
25+
public bool TryGetIncomingHandlerName([NotNullWhen(true)] out string? incomingHandlerName)
26+
{
27+
incomingHandlerName = _incomingHandlerName;
28+
return incomingHandlerName is not null;
29+
}
2530
}

src/Components/Endpoints/src/FormMapping/HttpContextFormValueMapper.cs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,58 @@ public HttpContextFormValueMapper(HttpContextFormDataProvider formData)
2222
_formData = formData;
2323
}
2424

25-
public bool CanMap(Type valueType, string? formName = null)
25+
public bool CanMap(Type valueType, string scopeName, string? formName)
2626
{
27-
if (formName == null)
27+
// We must always match on scope
28+
if (!_formData.TryGetIncomingHandlerName(out var incomingScopeQualifiedFormName)
29+
|| !MatchesScope(incomingScopeQualifiedFormName, scopeName, out var incomingFormName))
2830
{
29-
return _options.ResolveConverter(valueType) != null;
31+
return false;
3032
}
31-
else
33+
34+
// Matching on formname is optional, enforced only if a nonempty form name was demanded by the receiver
35+
if (formName is not null && !incomingFormName.Equals(formName, StringComparison.Ordinal))
3236
{
33-
var result = _formData.IsFormDataAvailable &&
34-
string.Equals(formName, _formData.Name, StringComparison.Ordinal) &&
35-
_options.ResolveConverter(valueType) != null;
37+
return false;
38+
}
39+
40+
return _options.ResolveConverter(valueType) is not null;
41+
}
3642

37-
return result;
43+
private static bool MatchesScope(string incomingScopeQualifiedFormName, string currentMappingScopeName, out ReadOnlySpan<char> incomingFormName)
44+
{
45+
if (incomingScopeQualifiedFormName.StartsWith('['))
46+
{
47+
// The scope-qualified name is in the form "[scopename]formname", so validate that the [scopename]
48+
// prefix matches and return the formname part
49+
var incomingScopeQualifiedFormNameSpan = incomingScopeQualifiedFormName.AsSpan();
50+
if (incomingScopeQualifiedFormNameSpan[1..].StartsWith(currentMappingScopeName, StringComparison.Ordinal)
51+
&& incomingScopeQualifiedFormName.Length >= currentMappingScopeName.Length + 2
52+
&& incomingScopeQualifiedFormName[currentMappingScopeName.Length + 1] == ']')
53+
{
54+
incomingFormName = incomingScopeQualifiedFormNameSpan[(currentMappingScopeName.Length + 1)..];
55+
return true;
56+
}
3857
}
58+
else
59+
{
60+
// The scope-qualified name is in the form "formname", so validating that the scopename matches
61+
// means checking that it's empty
62+
if (string.IsNullOrEmpty(currentMappingScopeName))
63+
{
64+
incomingFormName = incomingScopeQualifiedFormName;
65+
return true;
66+
}
67+
}
68+
69+
incomingFormName = default;
70+
return false;
3971
}
4072

4173
public void Map(FormValueMappingContext context)
4274
{
4375
// This will func to a proper binder
44-
if (!CanMap(context.ValueType, context.FormName))
76+
if (!CanMap(context.ValueType, context.MappingScopeName, context.RestrictToFormName))
4577
{
4678
context.SetResult(null);
4779
}

src/Components/Web/src/Forms/Mapping/FormMappingContext.cs

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ public sealed class FormMappingContext
1414
private List<KeyValuePair<string, FormMappingError>>? _pendingErrors;
1515
private Dictionary<string, Dictionary<string, FormMappingError>>? _errorsByFormName;
1616

17-
internal FormMappingContext(string name)
17+
internal FormMappingContext(string mappingScopeName)
1818
{
19-
ArgumentNullException.ThrowIfNull(name);
20-
Name = name;
19+
ArgumentNullException.ThrowIfNull(mappingScopeName);
20+
MappingScopeName = mappingScopeName;
2121
}
2222

2323
/// <summary>
24-
/// The context name.
24+
/// The mapping scope name.
2525
/// </summary>
26-
public string Name { get; }
26+
public string MappingScopeName { get; }
2727

2828
/// <summary>
2929
/// Retrieves the list of errors for a given model key.
@@ -92,18 +92,6 @@ public IEnumerable<FormMappingError> GetAllErrors(string formName)
9292
_errorsByFormName?.TryGetValue(formName, out var formErrors) == true &&
9393
formErrors.TryGetValue(key, out var mappingError) ? mappingError.AttemptedValue : null;
9494

95-
internal string GetScopeQualifiedFormName(string? formHandlerName)
96-
{
97-
if (string.IsNullOrEmpty(Name))
98-
{
99-
return string.IsNullOrEmpty(formHandlerName) ? string.Empty : formHandlerName;
100-
}
101-
else
102-
{
103-
return string.IsNullOrEmpty(formHandlerName) ? Name : $"{Name}.{formHandlerName}";
104-
}
105-
}
106-
10795
internal void AddError(string key, FormattableString error, string? attemptedValue)
10896
{
10997
_errors ??= new Dictionary<string, FormMappingError>();

src/Components/Web/src/Forms/Mapping/FormMappingScope.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ Task IComponent.SetParametersAsync(ParameterView parameters)
4343
{
4444
throw new InvalidOperationException($"The {nameof(FormMappingScope)} component requires a nonempty {nameof(Name)} parameter value.");
4545
}
46+
else if (Name.StartsWith('['))
47+
{
48+
// We use "scope-qualified form name starts with [" as a signal that there's a nonempty scope, so don't let the name itself start that way
49+
// Alternatively we could avoid packing both the scope and form name into a single string, or use some encoding. However it's very unlikely
50+
// this restriction will affect anyone, and the exact representation is an internal implementation detail.
51+
throw new InvalidOperationException($"The mapping scope name '{Name}' starts with a disallowed character.");
52+
}
4653

4754
_cascadingValueSupplier = new SupplyParameterFromFormValueProvider(FormValueModelBinder, Name);
4855
}

src/Components/Web/src/Forms/Mapping/FormValueMappingContext.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,31 @@ public class FormValueMappingContext
1313
/// <summary>
1414
/// Initializes a new instance of <see cref="FormValueMappingContext"/>.
1515
/// </summary>
16-
/// <param name="formName">The name of the form to map data from.</param>
16+
/// <param name="mappingScopeName">The name of the current <see cref="FormMappingScope"/>. Values will only be mapped if the incoming data corresponds to this scope name.</param>
17+
/// <param name="restrictToFormName">If set, indicates that the mapping should only receive values if the incoming form matches this name. If null, the mapping should receive data from any form in the mapping scope.</param>
1718
/// <param name="valueType">The <see cref="Type"/> of the value to map.</param>
1819
/// <param name="parameterName">The name of the parameter to map data to.</param>
19-
public FormValueMappingContext(string formName, Type valueType, string parameterName)
20+
public FormValueMappingContext(string mappingScopeName, string? restrictToFormName, Type valueType, string parameterName)
2021
{
21-
ArgumentNullException.ThrowIfNull(formName, nameof(formName));
22+
ArgumentNullException.ThrowIfNull(mappingScopeName, nameof(mappingScopeName));
2223
ArgumentNullException.ThrowIfNull(valueType, nameof(valueType));
2324
ArgumentNullException.ThrowIfNull(parameterName, nameof(parameterName));
2425

25-
FormName = formName;
26+
MappingScopeName = mappingScopeName;
27+
RestrictToFormName = restrictToFormName;
2628
ParameterName = parameterName;
2729
ValueType = valueType;
2830
}
2931

3032
/// <summary>
31-
/// Gets the name of the form to map data from.
33+
/// Gets the name of the current <see cref="FormMappingScope"/>.
3234
/// </summary>
33-
public string FormName { get; }
35+
public string MappingScopeName { get; }
36+
37+
/// <summary>
38+
/// If set, indicates that the mapping should only receive values if the incoming form matches this name. If null, the mapping should receive data from any form in the mapping scope.
39+
/// </summary>
40+
public string? RestrictToFormName { get; }
3441

3542
/// <summary>
3643
/// Gets the name of the parameter to map data to.

src/Components/Web/src/Forms/Mapping/IFormValueMapper.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ public interface IFormValueMapper
1212
/// Determines whether the specified value type can be mapped.
1313
/// </summary>
1414
/// <param name="valueType">The <see cref="Type"/> for the value to map.</param>
15-
/// <param name="scopeQualifiedFormName">The form name to map data from or null to only validate the type can be mapped.</param>
15+
/// <param name="scopeName">The name of the current <see cref="FormMappingScope"/>.</param>
16+
/// <param name="formName">The form name, if values should only be provided for that form, or null to allow values from any form within the scope.</param>
1617
/// <returns><c>true</c> if the value type can be mapped; otherwise, <c>false</c>.</returns>
17-
bool CanMap(Type valueType, string? scopeQualifiedFormName = null);
18+
bool CanMap(Type valueType, string scopeName, string? formName);
1819

1920
/// <summary>
2021
/// Maps the form value with the specified name to a value of the specified type.

src/Components/Web/src/Forms/Mapping/SupplyParameterFromFormValueProvider.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
4040
// We also supply values for [SupplyValueFromForm]
4141
if (_formValueMapper is not null && parameterInfo.Attribute is SupplyParameterFromFormAttribute supplyParameterFromFormAttribute)
4242
{
43-
var scopeQualifiedFormName = _mappingContext.GetScopeQualifiedFormName(supplyParameterFromFormAttribute.Handler);
44-
return _formValueMapper.CanMap(parameterInfo.PropertyType, scopeQualifiedFormName);
43+
return _formValueMapper.CanMap(parameterInfo.PropertyType, MappingScopeName, supplyParameterFromFormAttribute.Handler);
4544
}
4645

4746
return false;
@@ -73,15 +72,14 @@ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in Cascading
7372
internal static object? GetFormPostValue(IFormValueMapper formValueMapper, FormMappingContext? mappingContext, in CascadingParameterInfo parameterInfo, SupplyParameterFromFormAttribute supplyParameterFromFormAttribute)
7473
{
7574
Debug.Assert(mappingContext != null);
76-
var scopeQualifiedFormName = mappingContext.GetScopeQualifiedFormName(supplyParameterFromFormAttribute.Handler);
7775

7876
var parameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName;
79-
var handler = ((SupplyParameterFromFormAttribute)parameterInfo.Attribute).Handler;
80-
Action<string, FormattableString, string?> errorHandler = string.IsNullOrEmpty(handler) ?
77+
var restrictToFormName = ((SupplyParameterFromFormAttribute)parameterInfo.Attribute).Handler;
78+
Action<string, FormattableString, string?> errorHandler = string.IsNullOrEmpty(restrictToFormName) ?
8179
mappingContext.AddError :
82-
(name, message, value) => mappingContext.AddError(scopeQualifiedFormName, parameterName, message, value);
80+
(name, message, value) => mappingContext.AddError(restrictToFormName, parameterName, message, value);
8381

84-
var context = new FormValueMappingContext(scopeQualifiedFormName, parameterInfo.PropertyType, parameterName)
82+
var context = new FormValueMappingContext(mappingContext.MappingScopeName, restrictToFormName, parameterInfo.PropertyType, parameterName)
8583
{
8684
OnError = errorHandler,
8785
MapErrorToContainer = mappingContext.AttachParentValue

src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.HtmlWriting.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ protected bool TryGetScopeQualifiedEventName(int componentId, string eventName,
194194
{
195195
if (FindFormMappingContext(componentId) is { } formMappingContext)
196196
{
197-
scopeQualifiedEventName = formMappingContext.GetScopeQualifiedFormName(eventName);
197+
scopeQualifiedEventName = CreateScopeQualifiedFormName(formMappingContext, eventName);
198198
return true;
199199
}
200200
else
@@ -215,6 +215,19 @@ protected bool TryGetScopeQualifiedEventName(int componentId, string eventName,
215215
return (FormMappingContext?)supplier?.GetCurrentValue(_findFormMappingContext);
216216
}
217217

218+
internal string CreateScopeQualifiedFormName(FormMappingContext mappingContext, string? formHandlerName)
219+
{
220+
var mappingScopeName = mappingContext.MappingScopeName;
221+
if (string.IsNullOrEmpty(mappingScopeName))
222+
{
223+
return formHandlerName ?? string.Empty;
224+
}
225+
else
226+
{
227+
return $"[{mappingScopeName}]{formHandlerName ?? string.Empty}";
228+
}
229+
}
230+
218231
private static bool TryFindEnclosingElementFrame(ArrayRange<RenderTreeFrame> frames, int frameIndex, out int result)
219232
{
220233
while (--frameIndex >= 0)

src/Components/Web/src/PublicAPI.Unshipped.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Microsoft.AspNetCore.Components.Forms.FormMappingContext.GetAttemptedValue(strin
2525
Microsoft.AspNetCore.Components.Forms.FormMappingContext.GetAttemptedValue(string! key) -> string?
2626
Microsoft.AspNetCore.Components.Forms.FormMappingContext.GetErrors(string! formName, string! key) -> Microsoft.AspNetCore.Components.Forms.Mapping.FormMappingError?
2727
Microsoft.AspNetCore.Components.Forms.FormMappingContext.GetErrors(string! key) -> Microsoft.AspNetCore.Components.Forms.Mapping.FormMappingError?
28-
Microsoft.AspNetCore.Components.Forms.FormMappingContext.Name.get -> string!
28+
Microsoft.AspNetCore.Components.Forms.FormMappingContext.MappingScopeName.get -> string!
2929
Microsoft.AspNetCore.Components.Forms.FormMappingScope
3030
Microsoft.AspNetCore.Components.Forms.FormMappingScope.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.Forms.FormMappingContext!>!
3131
Microsoft.AspNetCore.Components.Forms.FormMappingScope.ChildContent.set -> void
@@ -42,18 +42,19 @@ Microsoft.AspNetCore.Components.Forms.Mapping.FormMappingError.ErrorMessages.get
4242
Microsoft.AspNetCore.Components.Forms.Mapping.FormMappingError.Name.get -> string!
4343
Microsoft.AspNetCore.Components.Forms.Mapping.FormMappingError.Path.get -> string!
4444
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext
45-
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.FormName.get -> string!
46-
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.FormValueMappingContext(string! formName, System.Type! valueType, string! parameterName) -> void
45+
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.FormValueMappingContext(string! mappingScopeName, string? restrictToFormName, System.Type! valueType, string! parameterName) -> void
4746
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.MapErrorToContainer.get -> System.Action<string!, object!>?
4847
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.MapErrorToContainer.set -> void
48+
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.MappingScopeName.get -> string!
4949
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.OnError.get -> System.Action<string!, System.FormattableString!, string?>?
5050
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.OnError.set -> void
5151
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.ParameterName.get -> string!
52+
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.RestrictToFormName.get -> string?
5253
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.Result.get -> object?
5354
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.SetResult(object? result) -> void
5455
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.ValueType.get -> System.Type!
5556
Microsoft.AspNetCore.Components.Forms.Mapping.IFormValueMapper
56-
Microsoft.AspNetCore.Components.Forms.Mapping.IFormValueMapper.CanMap(System.Type! valueType, string? scopeQualifiedFormName = null) -> bool
57+
Microsoft.AspNetCore.Components.Forms.Mapping.IFormValueMapper.CanMap(System.Type! valueType, string! scopeName, string? formName) -> bool
5758
Microsoft.AspNetCore.Components.Forms.Mapping.IFormValueMapper.Map(Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext! context) -> void
5859
Microsoft.AspNetCore.Components.Forms.Mapping.SupplyParameterFromFormServiceCollectionExtensions
5960
Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer

src/Components/Web/test/Forms/Mapping/FormMappingContextTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ public class FormMappingContextTest
1111
public void CanCreate_MappingContext_WithDefaultName()
1212
{
1313
var context = new FormMappingContext("");
14-
Assert.Equal("", context.Name);
14+
Assert.Equal("", context.MappingScopeName);
1515
}
1616

1717
[Fact]
1818
public void CanCreate_MappingContext_WithName()
1919
{
2020
var context = new FormMappingContext("name");
21-
Assert.Equal("name", context.Name);
21+
Assert.Equal("name", context.MappingScopeName);
2222
}
2323
}

src/Components/Web/test/Forms/Mapping/FormMappingScopeTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public void SuppliesMappingContext()
4141

4242
// Assert
4343
Assert.NotNull(capturedContext);
44-
Assert.Equal("named-context", capturedContext.Name);
44+
Assert.Equal("named-context", capturedContext.MappingScopeName);
4545
}
4646

4747
[Fact]
@@ -71,7 +71,7 @@ public void CanNestToOverride()
7171

7272
// Assert
7373
Assert.NotNull(capturedContext);
74-
Assert.Equal("child-context", capturedContext.Name);
74+
Assert.Equal("child-context", capturedContext.MappingScopeName);
7575
}
7676

7777
[Theory]

src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ActionForm.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ RenderFragment RenderWithNamedContext(FormMappingContext context)
4646
void RenderFormContents(RenderTreeBuilder builder, FormMappingContext? bindingContext)
4747
{
4848
builder.OpenElement(0, "form");
49-
builder.AddAttribute(1, "name", bindingContext.Name);
49+
builder.AddAttribute(1, "name", bindingContext.MappingScopeName);
5050
builder.AddAttribute(6, "method", "POST");
5151
builder.AddAttribute(7, "onsubmit", async () => await OnSubmit.InvokeAsync(bindingContext));
5252

0 commit comments

Comments
 (0)