Skip to content

Commit 2983a98

Browse files
authored
[Blazor] Adds support for updating the parameter names as well as generating names inside child components (#49224)
* Support updating/changing the binding prefix * Support server-side forms that can be broken down into child components.
1 parent 8b2fd3f commit 2983a98

26 files changed

+721
-37
lines changed

src/Components/Components/src/Rendering/RenderTreeBuilder.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public void AddAttribute(int sequence, string name)
177177

178178
if (TrackNamedEventHandlers && string.Equals(name, "@onsubmit:name", StringComparison.Ordinal))
179179
{
180-
_entries.AppendAttribute(sequence, name, "");
180+
SetEventHandlerName("");
181181
}
182182

183183
_entries.AppendAttribute(sequence, name, BoxedTrue);
@@ -385,10 +385,12 @@ public void AddAttribute(int sequence, string name, object? value)
385385
{
386386
if (TrackNamedEventHandlers && string.Equals(name, "@onsubmit:name", StringComparison.Ordinal))
387387
{
388-
_entries.AppendAttribute(sequence, name, value);
388+
SetEventHandlerName("");
389+
}
390+
else
391+
{
392+
_entries.AppendAttribute(sequence, name, BoxedTrue);
389393
}
390-
391-
_entries.AppendAttribute(sequence, name, BoxedTrue);
392394
}
393395
else
394396
{

src/Components/Components/test/CascadingParameterStateTest.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ class FormParametersComponent : TestComponentBase
468468

469469
class FormParametersComponentWithName : TestComponentBase
470470
{
471-
[SupplyParameterFromForm(Name = "some-name")] public string FormParameter { get; set; }
471+
[SupplyParameterFromForm(Handler = "some-name")] public string FormParameter { get; set; }
472472
}
473473

474474
class ComponentWithNoCascadingParams : TestComponentBase
@@ -554,4 +554,10 @@ public sealed class SupplyParameterFromFormAttribute : CascadingParameterAttribu
554554
/// the form data and decide whether or not the value needs to be bound.
555555
/// </summary>
556556
public override string Name { get; set; }
557+
558+
/// <summary>
559+
/// Gets or sets the name for the handler. The name is used to match
560+
/// the form data and decide whether or not the value needs to be bound.
561+
/// </summary>
562+
public string Handler { get; set; }
557563
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace BlazorUnitedApp.Data;
5+
6+
public class Address
7+
{
8+
public string Street { get; set; } = string.Empty;
9+
public string City { get; set; } = string.Empty;
10+
public string State { get; set; } = string.Empty;
11+
public string Zip { get; set; } = string.Empty;
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace BlazorUnitedApp.Data;
5+
6+
public class Customer
7+
{
8+
public string Name { get; set; } = string.Empty;
9+
public Address BillingAddress { get; set; } = new Address();
10+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@inherits Editor<Address>
2+
@using BlazorUnitedApp.Data
3+
4+
<div>
5+
<label>
6+
<span>Street</span>
7+
<InputText @bind-Value="Value.Street" />
8+
<ValidationMessage For="() => Value.Street" />
9+
</label>
10+
</div>
11+
<div>
12+
<label>
13+
<span>State</span>
14+
<InputText @bind-Value="Value.State" />
15+
<ValidationMessage For="() => Value.State" />
16+
</label>
17+
</div>
18+
<div>
19+
<label>
20+
<span>Zip</span>
21+
<InputText @bind-Value="Value.Zip" />
22+
<ValidationMessage For="() => Value.Zip" />
23+
</label>
24+
</div>
25+
<div>
26+
<label>
27+
<span>City</span>
28+
<InputText @bind-Value="Value.City" />
29+
<ValidationMessage For="() => Value.City" />
30+
</label>
31+
</div>
Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,40 @@
11
@page "/"
2+
@using BlazorUnitedApp.Data;
23
<PageTitle>Index</PageTitle>
34

4-
<h1>@Value?.Parameter</h1>
5-
6-
<EditForm Model="Value?.Parameter">
7-
<InputText @bind-Value="Value!.Parameter" />
5+
<EditForm Model="Value" method="POST" OnSubmit="DisplayCustomer">
6+
<div>
7+
<label>
8+
<span>Name</span>
9+
<InputText @bind-Value="Value!.Name" />
10+
</label>
11+
</div>
12+
<AddressEditor @bind-Value="Value.BillingAddress" />
813
<input type="submit" value="Send" />
914
</EditForm>
1015

1116
@if (_submitted)
1217
{
13-
<p>Submited.</p>
18+
<!-- Display customer data -->
19+
<h3>Customer</h3>
20+
<p>Name: @Value!.Name</p>
21+
<p>Street: @Value.BillingAddress.Street</p>
22+
<p>City: @Value.BillingAddress.City</p>
23+
<p>State: @Value.BillingAddress.State</p>
24+
<p>Zip: @Value.BillingAddress.Zip</p>
1425
}
1526

16-
@code{
17-
[SupplyParameterFromForm] Data? Value { get; set; }
27+
@code {
28+
29+
public void DisplayCustomer()
30+
{
31+
_submitted = true;
32+
}
33+
34+
[SupplyParameterFromForm] Customer? Value { get; set; }
1835

1936
protected override void OnInitialized() => Value ??= new();
2037

2138
bool _submitted = false;
2239
public void Submit() => _submitted = true;
23-
24-
public class Data
25-
{
26-
public string Parameter { get; set; } = "";
27-
}
2840
}

src/Components/Shared/src/ExpressionFormatting/ExpressionFormatter.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,16 @@ public static void ClearCache()
2424
}
2525

2626
public static string FormatLambda(LambdaExpression expression)
27+
{
28+
return FormatLambda(expression, prefix: null);
29+
}
30+
31+
public static string FormatLambda(LambdaExpression expression, string? prefix = null)
2732
{
2833
var builder = new ReverseStringBuilder(stackalloc char[StackAllocBufferSize]);
2934
var node = expression.Body;
3035
var wasLastExpressionMemberAccess = false;
36+
var wasLastExpressionIndexer = false;
3137

3238
while (node is not null)
3339
{
@@ -45,20 +51,32 @@ public static string FormatLambda(LambdaExpression expression)
4551
throw new InvalidOperationException("Method calls cannot be formatted.");
4652
}
4753

54+
node = methodCallExpression.Object;
55+
if (prefix != null && node is ConstantExpression)
56+
{
57+
break;
58+
}
59+
4860
if (wasLastExpressionMemberAccess)
4961
{
5062
wasLastExpressionMemberAccess = false;
5163
builder.InsertFront(".");
5264
}
65+
wasLastExpressionIndexer = true;
5366

5467
builder.InsertFront("]");
5568
FormatIndexArgument(methodCallExpression.Arguments[0], ref builder);
5669
builder.InsertFront("[");
57-
node = methodCallExpression.Object;
70+
5871
break;
5972

6073
case ExpressionType.ArrayIndex:
6174
var binaryExpression = (BinaryExpression)node;
75+
node = binaryExpression.Left;
76+
if (prefix != null && node is ConstantExpression)
77+
{
78+
break;
79+
}
6280

6381
if (wasLastExpressionMemberAccess)
6482
{
@@ -69,23 +87,27 @@ public static string FormatLambda(LambdaExpression expression)
6987
builder.InsertFront("]");
7088
FormatIndexArgument(binaryExpression.Right, ref builder);
7189
builder.InsertFront("[");
72-
node = binaryExpression.Left;
90+
wasLastExpressionIndexer = true;
7391
break;
7492

7593
case ExpressionType.MemberAccess:
7694
var memberExpression = (MemberExpression)node;
77-
var nextNode = memberExpression.Expression;
95+
node = memberExpression.Expression;
96+
if (prefix != null && node is ConstantExpression)
97+
{
98+
break;
99+
}
78100

79101
if (wasLastExpressionMemberAccess)
80102
{
81103
builder.InsertFront(".");
82104
}
83105
wasLastExpressionMemberAccess = true;
106+
wasLastExpressionIndexer = false;
84107

85108
var name = memberExpression.Member.Name;
86109
builder.InsertFront(name);
87110

88-
node = nextNode;
89111
break;
90112

91113
default:
@@ -95,6 +117,15 @@ public static string FormatLambda(LambdaExpression expression)
95117
}
96118
}
97119

120+
if (prefix != null)
121+
{
122+
if (!builder.Empty && !wasLastExpressionIndexer)
123+
{
124+
builder.InsertFront(".");
125+
}
126+
builder.InsertFront(prefix);
127+
}
128+
98129
var result = builder.ToString();
99130

100131
builder.Dispose();

src/Components/Shared/src/ExpressionFormatting/ReverseStringBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public ReverseStringBuilder(Span<char> initialBuffer)
3333
_nextEndIndex = _currentBuffer.Length;
3434
}
3535

36+
public bool Empty => _nextEndIndex == _currentBuffer.Length;
37+
3638
public void InsertFront(scoped ReadOnlySpan<char> span)
3739
{
3840
var startIndex = _nextEndIndex - span.Length;

src/Components/Web/src/Binding/CascadingFormModelBindingProvider.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ protected internal override bool CanSupplyValue(ModelBindingContext? bindingCont
4545
Debug.Assert(bindingContext != null);
4646
var (formName, valueType) = GetFormNameAndValueType(bindingContext, parameterInfo);
4747

48-
var parameterName = parameterInfo.Attribute.Name;
49-
50-
Action<string, FormattableString, string?> errorHandler = string.IsNullOrEmpty(parameterName) ?
48+
var parameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName;
49+
var handler = ((SupplyParameterFromFormAttribute)parameterInfo.Attribute).Handler;
50+
Action<string, FormattableString, string?> errorHandler = string.IsNullOrEmpty(handler) ?
5151
bindingContext.AddError :
52-
(name, message, value) => bindingContext.AddError(parameterName, name, message, value);
52+
(name, message, value) => bindingContext.AddError(formName, parameterName, message, value);
5353

54-
var context = new FormValueSupplierContext(formName!, valueType, parameterInfo.PropertyName)
54+
var context = new FormValueSupplierContext(formName!, valueType, parameterName)
5555
{
5656
OnError = errorHandler,
5757
MapErrorToContainer = bindingContext.AttachParentValue
@@ -65,7 +65,7 @@ protected internal override bool CanSupplyValue(ModelBindingContext? bindingCont
6565
private static (string FormName, Type ValueType) GetFormNameAndValueType(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo)
6666
{
6767
var valueType = parameterInfo.PropertyType;
68-
var valueName = parameterInfo.Attribute.Name;
68+
var valueName = ((SupplyParameterFromFormAttribute)parameterInfo.Attribute).Handler;
6969
var formName = string.IsNullOrEmpty(valueName) ?
7070
(bindingContext?.Name) :
7171
ModelBindingContext.Combine(bindingContext, valueName);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq.Expressions;
5+
using Microsoft.AspNetCore.Components.Rendering;
6+
7+
namespace Microsoft.AspNetCore.Components.Forms;
8+
9+
/// <summary>
10+
/// A component used for editing a value of type <typeparamref name="T"/>.
11+
/// </summary>
12+
/// <typeparam name="T"></typeparam>
13+
public abstract class Editor<T> : ComponentBase, ICascadingValueSupplier
14+
{
15+
private HtmlFieldPrefix? _value;
16+
17+
/// <summary>
18+
/// The value for the component.
19+
/// </summary>
20+
[Parameter] public T Value { get; set; } = default!;
21+
22+
/// <summary>
23+
/// An expression that represents the value for the component.
24+
/// </summary>
25+
[Parameter] public Expression<Func<T>> ValueExpression { get; set; } = default!;
26+
27+
/// <summary>
28+
/// A callback that gets invoked when the value changes.
29+
/// </summary>
30+
[Parameter] public EventCallback<T> ValueChanged { get; set; } = default!;
31+
32+
[CascadingParameter] private HtmlFieldPrefix FieldPrefix { get; set; } = default!;
33+
34+
bool ICascadingValueSupplier.IsFixed => true;
35+
36+
/// <summary>
37+
/// Returns the name for the specified <paramref name="expression"/> in the current context.
38+
/// </summary>
39+
/// <param name="expression">The expression to use to compute the name.</param>
40+
/// <returns>The name for the specified <paramref name="expression"/> in the current context.</returns>
41+
/// <remarks>The provided <paramref name="expression"/> must be a member expression with <see cref="Editor{T}.Value"/> as it source.</remarks>
42+
protected string NameFor(LambdaExpression expression) => _value!.GetFieldName(expression);
43+
44+
/// <inheritdoc />
45+
protected override void OnParametersSet()
46+
{
47+
if (ValueExpression == null)
48+
{
49+
throw new InvalidOperationException($"{GetType()} requires a value for the 'ValueExpression' " +
50+
"parameter. Normally this is provided automatically when using 'bind-Value'.");
51+
}
52+
53+
_value = FieldPrefix != null ? FieldPrefix.Combine(ValueExpression) : new HtmlFieldPrefix(ValueExpression);
54+
}
55+
56+
bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) =>
57+
parameterInfo.PropertyType == typeof(HtmlFieldPrefix);
58+
59+
object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
60+
{
61+
return ((ICascadingValueSupplier)this).CanSupplyValue(parameterInfo) ? _value : null;
62+
}
63+
64+
void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
65+
{
66+
throw new InvalidOperationException($"Cannot subscribe to a {typeof(HtmlFieldPrefix).Name}.");
67+
}
68+
69+
void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
70+
{
71+
throw new InvalidOperationException($"Cannot subscribe to a {typeof(HtmlFieldPrefix).Name}.");
72+
}
73+
}

0 commit comments

Comments
 (0)