-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Blazor SSR traditional form post handling #48760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I've updated the description above and would love to check this is what people are expecting before I start on any implementation. I think the description here merges the latest stuff from this issue, #47804, and the "form scenarios" exercise I put together a few weeks ago. The biggest open question for me is whether or not we should really be generating form field MyForm.razor <EditForm Model="@Person">
<AddressEditor Title="Home address" Address="@Person.HomeAddress" />
<AddressEditor Title="Work address" Address="@Person.WorkAddress" />
</EditForm> AddressEditor.razor: <h2>@Title</h2>
<InputText @bind-Value="Address.Line1" />
<InputText @bind-Value="Address.City" />
<InputText @bind-Value="Address.PostCode" />
@code {
[Parameter] public AddressData Address { get; set; }
} This will work perfectly as an interactive form. But it won't generate the correct field names for SSR, because MyForm.razor <EditForm Model="@Person">
<FormContext For="@(() => Person.HomeAddress)" context="formContext">
<AddressEditor Title="Home address" Address="@formContext" />
</FormContext>
<FormContext For="@(() => Person.WorkAddress)" context="formContext">
<AddressEditor Title="Work address" Address="@formContext" />
</FormContext>
</EditForm> AddressEditor.razor: <h2>@Title</h2>
<InputText @bind-Value="Address.Line1" />
<InputText @bind-Value="Address.City" />
<InputText @bind-Value="Address.PostCode" />
@code {
[Parameter] public AddressData Address { get; set; }
} ... but that still doesn't work because even if @* Explicitly remove the wrong part of the field name *@
<FieldNameContext For="@(() => Address)">
<InputText @bind-Value="Address.Line1" />
...
</FieldNameContext>
@* Or, explicitly remove it on the InputText *@
<InputText @bind-Value="Address.Line1" SkipPrefix="@(() => Address)" />
@* Or, explicitly state just the part after the cascaded prefix (probably the most intelligible choice of these) *@
<InputText Name="Line1" @bind-Value="Address.Line1" />
@* Or we bake in some hidden magic that tries to guess what you're doing based on whether some prefix
of your lambda seems to match with a [Parameter] property (not [SupplyParameterFromForm]), and we just
hope people don't try to do anything more custom in terms of how they pass values to child components,
which will be very leaky *@ Altogether none of these seem great, so I'm unclear on whether we're doing people a service by trying to generate names or a disservice because as soon as it goes wrong, there's no reasonable solution besides overriding it manually anyway (and the way in which it goes wrong would be nonobvious and seem like a framework bug). I would be OK with just not trying to solve this problem and expecting people to manage names manually until or if we can think of a nonleaky solution. Or possibly have something like Opinions? |
cc @dotnet/aspnet-blazor-eng for feedback here |
@dotnet/aspnet-blazor-eng Last call for design feedback! I'll start implementing this tomorrow. |
One other possible solution I thought of for the name generation logic would be that if you have something like: <FormContext For="@(() => Person.HomeAddress)" context="formContext">
<AddressEditor Title="Home address" Address="@formContext" />
</FormContext> and <h2>@Title</h2>
<InputText @bind-Value="Address.Line1" />
<InputText @bind-Value="Address.City" />
<InputText @bind-Value="Address.PostCode" />
@code {
[Parameter] public AddressData Address { get; set; }
} ... then the formatting logic could check whether the first token in the lambda has a value reference-equal with the "form context" value and if so, skip it. That is, it would evaluate Realistically I don't think we'd do that since it involves compiling a whole new lambda expression, which (1) could be harmful for perf and I'm unsure if it could even be cached based on what we know and (2) would likely never align with AOT plans. It also doesn't work in more general cases like if you have a custom editor that receives two objects and edits them both. So altogether I don't have a real solution for this and am really questioning what we are going to tell people who try to use forms in nontrivial cases. |
Given the potential for customers to misunderstand the mechanics of field name generation, I would definitely be fine with solving that problem at a later point and requiring manual field name management. Or if there was some way to enable it for basic scenarios while making the error experience clear for advanced scenarios (like nested components), I think that would be acceptable too. |
|
More feedback, based on the scenarios you raised a while ago: Scenario 1 Reading the form data procedurally @inject IHttpContextAccessor HttpContextAccessor
@if (Request.HasFormContentType)
{
foreach (var kvp in Request.Form)
{
<p>@kvp.Key: @kvp.Value</p>
}
}
@code {
HttpRequest Request => HttpContextAccessor.HttpContext!.Request;
} ... then curl -v https://localhost:7247/handlepost -d "key1=value1&key2=value2" Error: No named event handler was captured for ''. Workaround: Add this fake form, then it works: <form @onsubmit="@(() => {})">@{ __builder.SetEventHandlerName(""); }
Why? This seems like a pit of failure. If you don't have a form, this opens up to processing data in unexpected ways. Razor Pages will 404 if you don't have an This will also never work because you need antiforgery Also, what is the scenario here, you typically want to take an action when you receive data, how do you do that. OnInitializedAsync? How is that better than handling it inside the submit handler? Actual code
Scenario 2 Model binding scalar values <p>Name: @Name</p>
<p>Age: @Age</p>
@code {
[SupplyParameterFromForm] public string? Name { get; set; }
[SupplyParameterFromForm] public int Age { get; set; }
} This again needs antiforgery, and needs to go inside a form.
** Scenario 3 Model Binding scalar values**
It is unclear how this scenario works or how it scales up to Server/Webassembly scenarios.
Scenario 4 Model binding complex values
Scenario 5 Constructing names manually
Generating field names automatically Scenario 6 Plain form submit events
We don't have the SubmitEventArgs, but it can work just fine with the OnPost convention in the form.
Scenario 7
Challenge here is that you end up with N forms on the page. There is state associated with those forms, like validation, etc, that you can't simply ignore.
Alternatively, we disambiguate based on the handler + key, and we require you to setup a key that is
Scenario 8
This should work. Scenario 9 Edit form and validation This should work with the exception of splitting the form into multiple components, which is covered by Scenario 10: Isomorphic forms This works |
Thanks for the feedback. Regarding the "incoming POST without any form or submit handler" cases, those are not mainstream scenarios; they were more indicative of how we could untangle the web of concepts and allow each thing to work independently as basic machinery. It's not a mainstream scenario and so I'm fine with imposing a rule like "we only accept a POST if it comes with a handler name that matches something we can find on the page", which implies you must have a form with a name. It doesn't strictly imply you must have Quite a few of the desired experience improvements are in flight already, such as taking Following feedback, I think these are the updates I'm planning to do, hopefully as much as possible this week:
[1] This is about allowing basic old-school machinery like "using a form post to trigger an
There does have to be an option to provide a unique name (like dozens of other web dev scenarios where you have to give a unique-enough name, like for cookies, headers, route templates, parameter names, etc.) but I don't think it has to be hierarchical, as in combining arbitrarily many levels. A single optional cascaded name could be combined with any form name specified within a component. |
[1] This is about allowing basic old-school machinery like "using a form post to trigger an How do they map the event handler to the form to show the information in context? For example, if you have a grid, you might want to highlight the row with the error. It has nothing to do with edit form or anything "higher level", but the triggered action should be associated with a unique element, just as in HTML. This is about allowing basic old-school machinery like "using a form post to trigger an
Page.razor <h3>Table component</h3>
<div>
<table>
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Price</th>
<th>Remaining Units</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var product in _catalog.Products)
{
<tr @key="product">
<td>@product.Id</td>
<td>@product.Name</td>
<td>@product.Price</td>
<td>@product.RemainingUnits</td>
<td>
<CascadingModelBinder Name="DeleteProduct(product.Id)">
<DeleteProduct Product="@product" OnDeleteProduct="DeleteProduct" />
</CascadingModelBinder>
</td>
<td>
<CascadingModelBinder Name="UpdateRemainingItems(product.Id)">
<UpdateRemaingItems Product="@product"
OnUpdateRemainingItems="(remaining => UpdateRemainingItems(remaining.Product, remaining.Ammount))" />
</CascadingModelBinder>
</td>
</tr>
}
</tbody>
</table>
</div>
@code {
static Catalog _catalog = Catalog.Instance;
// This is just because we don't have string interpolation for component attributes.
public string DeleteProduct(int id) => $"DeleteProduct({id})";
public string UpdateRemainingItems(int id) => $"UpdateRemainingItems({id})";
public void DeleteProduct(Product product)
{
_catalog.Products.RemoveAll(p => p == product);
StateHasChanged();
}
public void UpdateRemainingItems(Product product, int ammount)
{
var remaining = _catalog.Products.SingleOrDefault(p => p == product);
remaining!.RemainingUnits -= ammount;
StateHasChanged();
}
} DeleteProduct.razor <form method="post" @onsubmit="() => OnDeleteProduct.InvokeAsync(Product)" @onsubmit:name="@BindingContext.Name" >
<AntiforgeryToken />
<input type="hidden" name="handler" value="@BindingContext.Name" />
<input type="submit" value="Delete" />
</form>
@code {
[CascadingParameter] public ModelBindingContext BindingContext { get; set; }
[Parameter] public Product Product { get; set; } = null!;
[Parameter] public EventCallback<Product> OnDeleteProduct { get; set; }
} UpdateRemainingItems.razor <form
method="post"
@onsubmit="() => OnUpdateRemainingItems.InvokeAsync(new(Product, Ammount))"
@onsubmit:name="@BindingContext.Name">
<AntiforgeryToken />
<input type="hidden" name="handler" value="@BindingContext.Name" />
<input type="text" name="Ammount" value="1" />
@{
var errors = BindingContext.GetAllErrors();
}
@if (errors?.Any() == true)
{
<input type="submit" value="Update remaining items" />
}
else
{
<input type="submit" value="Update remaining items" />
}
</form>
@code {
[CascadingParameter] public ModelBindingContext BindingContext { get; set; }
[Parameter] public Product Product { get; set; } = null!;
[SupplyParameterFromForm] public int Ammount { get; set; } = 1;
[Parameter] public EventCallback<RemainingItems> OnUpdateRemainingItems { get; set; }
public record RemainingItems(Product Product, int Ammount);
} The individual instances of Then the other part of it is that you might be building and shipping more complex components, so the context must be able to flow deep into the hierarchy without requiring users to do so. The above example shows that and some other things:
|
Only the extra layer of model binding context that it creates. It might be necessary but also maybe there could be an opportunity to make the model binding context more explicit and
It just simplifies things a lot for developers so they don't have to create the hidden field manually (or even care what magic name we use for the parameter). I'm totally fine with letting people optionally supply the handler name in other ways, such as querystring, but an automatic hidden field will likely do exactly what people want in 99% of cases.
Fair. I was only going to do this for forms that have both
Event handler delegates can be added, updated, or removed arbitrarily with normal interactive rendering and it will be hard to understand (and seem like a framework bug) if that's not the case with SSR. For example: <form method="POST" @onsubmit="@submitAction" @onsubmit:name="myform">
<AntiforgeryToken />
<input type="hidden" name="handler" value="myform" />
<input type="submit" value="Send" />
</form>
@code {
private Action? submitAction;
public void HandleFormBasic() { ... }
public void HandleFormBlueCheck() { ... }
protected override async Task OnInitializedAsync()
{
var user = await MyData.LoadUser();
submitAction = user.Name == "Elon" ? HandleFormBlueCheck : HandleFormBasic;
}
} If I'm understand the implementation correctly, then if There's already a whole system for tracking how events get added/updated/removed so ideally we'd follow a similar pattern or perhaps just take a more basic approach of lookup during dispatch so we always get the correct delegate. I totally accept it's possible there's no reasonable solution to this but since it's a core, low-level feature, I at least want to look into it.
Not certain what's meant by "the implementation of choice". Are you distinguishing between @onsubmit=lambda and @onsubmit=method? I'm only interested in doing the many-forms-one-handler thing if it follows easily and naturally from an updated event name tracking mechanism. I definitely won't do huge amounts of work/change for this if it doesn't follow naturally. AFAIK users can achieve the same outcome by wrapping their forms in a
Perhaps I was unclear in how I phrased things before, but I was trying to be explicit that I'm not disputing the usefulness of adding a All I am questioning is the relevance of it being fully hierarchical, as in allowing for arbitrarily many levels. AFAICT it is sufficient to just look at the closest |
Uh oh!
There was an error while loading. Please reload this page.
While we have now implemented support for
EditForm
usage in particular patterns, this issue is about enabling support for more basic, unopinionated form post handling. Capabilities to enable:<form>
via POST to the page you're onOnInitialized
runs[SupplyParameterFromForm("acceptsTerms")]
, otherwise defaults to property name[SupplyParameterFromForm(Handler = "myform")]
, otherwise accepts any POSTaction
being an internal URLmethod="post"
?@onsubmit
handler once the page reaches quiescence(target, MethodInfo)
pairs, invoke it@onsubmit
event exceptions in an ErrorBoundary-aware way (Blazor SSR form post handling: integrate errors with ErrorBoundary #47903)Input*
components@bind-Value=lambda
really generate a name by default? This only works in basic cases and we don't have a design proposal to scale this up to forms split into child components or if using custom edit components.CascadingModelBinder
, but change it not to be templated to avoid context clash)EditForm
ValidationMessageStore
(ideally this should be something you can do procedurally to anEditContext
too, rather than being exclusive to anEditForm
)@onsubmit
triggering as a plain<form>
, thereby triggering validation andOnValidSubmit
etcEarlier version of this issue description, all of which should be covered above
Expand
[SupplyParameterFromForm]
parameters<form>
post that does that<form>
trigger an@onsubmit
handler[SupplyParameterFromForm]
can work with simple scalar values, with the field name taken from the property name (no form name or prefix required)[SupplyParameterFromForm]
can work with complex objects given appropriate name prefixesmethod=post
and nonempty@onsubmit
)The text was updated successfully, but these errors were encountered: