-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
Handle forms with Blazor SSR #46688
Comments
Is it possible perhaps to link the form post to a specific method? You could possibly determine which method to wireup as an action method by looking at the edit form's The callback method could even allow an The model binding could check the callback parameter's I'm imagining something like this: <EditForm Model="@Form " method="post" OnSubmit="OnPost1">
<InputTextArea name="review" rows="5" Value="@Form.Text" />
<ValidationMessage For="() => Form.Text" />
</EditForm>
<EditForm Model="@Form2" method="post" OnSubmit="OnPost2">
@* Form2 usage here *@
</EditForm>
@code {
[Parameter] public TModel Form { get; set; }
[Parameter] public TModel2 Form2 { get; set; }
public async Task OnPost1(EditContext<TModel> context)
{
if(context.Validated())
{
// Logic goes here
}
}
public async Task OnPost2(EditContext<TModel2> context)
{
}
} |
@RyoukoKonpaku thanks for the input. We already have a similar design that I prototyped but that we have not shared yet as is still a WIP, there are some differences to what you are proposing, but the idea is similar. |
@javiercn Great to hear, I'll definitely be looking forward for those updates. |
Thanks for contacting us. We're moving this issue to the |
Blazor forms handlingThis document covers different approaches considered for handling form data within Server Rendered Blazor applications and the design considerations on the proposed approach. Goals
a. ASP.NET forms functionality
b. Progressive enhancement
c. Avoid exposing Blazor to HTTP
Scenarios1. Basic form@page /Recipes/New
@inject NavigationManager Navigation;
@inject RecipeRepository Recipes;
<div>
<EditForm Model="@NewRecipe" OnValidSubmit="AddRecipe">
<DataAnnotationsValidator />
<AntiforgeryToken />
<ValidationSummary />
<h2>Title</h2>
<InputText @bind-Value="@NewRecipe.Name" />
<ValidationMessage For="@(() => NewRecipe.Name)" />
<h2>Number of servings</h2>
<InputNumber @bind-Value="@NewRecipe.Servings" />
<ValidationMessage For="@(() => NewRecipe.Servings)" />
<h2>Instructions</h2>
<InputTextArea rows="5" name="text" placeholder="Write your instructions" @bind-Value="@NewRecipe.Instructions" />
<ValidationMessage For="@(() => NewRecipe.Instructions)" />
<button type="submit">Submit recipe</button>
</EditForm>
</div>
@code {
[SupplyFromForm] public Recipe NewRecipe { get; set; }
public override OnInitialized() => NewRecipe ??= new Recipe();
public async Task AddRecipe(SubmitEvent evt)
{
await Recipes.Add(NewRecipe);
Navigator.NavigateTo($"/Recipes/{Recipe.Id}");
}
} 2. Multiple forms@page /Recipes/Edit/{id}
@inject NavigationManager Navigation;
@inject RecipeRepository Recipes;
<div>
<EditForm Model="@Recipe" OnValidSubmit="UpdateRecipe">
<DataAnnotationsValidator />
<AntiforgeryToken />
<ValidationSummary />
<h2>Title</h2>
<InputText @bind-Value="@Recipe.Name" />
<ValidationMessage For="@(() => Recipe.Name)" />
<h2>Number of servings</h2>
<InputNumber @bind-Value="@Recipe.Servings" />
<ValidationMessage For="@(() => Recipe.Servings)" />
<h2>Instructions</h2>
<InputTextArea Name="Instructions" rows="5" name="text" placeholder="Write your instructions" @bind-Value="@Recipe.Instructions" />
<ValidationMessage For="@(() => Recipe.Instructions)" />
<button type="submit">Submit recipe</button>
</EditForm>
</div>
@for (var i = 0; i < recipe.Ingredients.Count; i++)
{
var index = i;
<div>
<EditForm Name="RemoveIngredient" Model="@recipe.Ingredients[index]" OnValidSubmit="RemoveIngredient">
<DataAnnotationsValidator />
<BoundValueValidator For="IngredientId" />
<AntiforgeryToken />
<ValidationSummary />
<ValidationRule
For="@(() => Recipe)"
Rule="@(recipe => recipe.Ingredients.Any(i => i.Id == IngredientsId))"
Message="The chosen ingredient does not exist.">
<InputHidden Name="IngredientId" Value="recipe.Ingredients[index].Id">
<button type="submit">Remove Ingredient</button>
</EditForm>
</div>
}
@code {
[SupplyFromForm] public Recipe Recipe { get; set; }
[SupplyFromForm("RemoveIngredient")] public int? IngredientId { get; set; }
[Parameter] public int Id { get; set; }
public override OnInitializedAsync()
{
Recipe ??= await Recipes.GetById(Id);
if(Recipe is null)
{
Navigator.NotFound();
}
}
public async Task RemoveIngredient()
{
Recipe.RemoveIngredient(IngredientId);
await Recipes.Update(Recipe);
}
public async Task UpdateRecipe(SubmitEvent evt)
{
await Recipes.Update(Recipe);
Navigator.NavigateTo($"/Recipes/{Recipe.Id}");
}
} 3. Handling model binding errors manually<EditForm ...>
<BoundValueValidator For="@Recipe" />
...
</EditForm>
@code{
[SupplyFromForm(FailOnError = false)] public Recipe Recipe { get; set; }
} WorkitemsEnable binding one form per documentForm appears inside the hierarchy of the "page", there is only 1 per document.
Enable multiple forms per documentForm can appear anywhere as long as is uniquely identified within the document hierarchy. Multiple forms can coexist.
Introduction to form handling in server rendered applicationsForm handling is the process by which the server deals with the user submitting a form via the browser, which potentially results on some state change for the app, as well as an updated UI. In traditional server rendered applications (like MVC and Razor Pages) the most common pattern for handling forms is POST -> Redirect -> GET. In this approach, the browser send a POST request to the server including the data in the form fields as When the server receives the request, it processes it and either responds with some updated HTML to indicate the result (this is common when there are validation failures) or redirects the user-agent (the browser) to load a different document that usually reflects the changes that were made. sequenceDiagram
actor User
participant Browser
participant Server
User->>+Browser: Click "send"
Browser->>Server: POST www.example.com/edit/5
deactivate Browser
alt data is invalid
activate Server
Server->>Browser: 200 OK<br><<Updated-markup-with-validation-errors>>
deactivate Server
Browser->>User: Updated document
else data is valid
activate Server
Server->>+Browser: 302 Redirect<br>Location: https://www.example.com/details/5
deactivate Server
activate Browser
Browser->>+Server: GET https://www.example.com/details/5
deactivate Browser
Server->>-Browser: 200 OK <<new-document-with-changes>>
Browser->>User: Updated document
end
Form handling patterns in interactive applicationsOn Blazor applications up to this day, there is no need to abide by this pattern, since the app is "stateful" it can transition between displaying data and editing it without requiring a navigation. However, even in the case of stateful applications is very common to follow this pattern, where editing data is handled in a separate page/area of the app represented by a unique URL and after an action has been successfully performed, the app redirects the user to a different part of the app using a client-side navigation. Even in the cases where this pattern is not followed for stateful applications, these apps implement some mechanism to deal with the "two" states (editing/reading) that are otherwise represented as separate documents. For example:
Blazor applications can also handle richer levels of interactivity, as they are not limited to a set of static form fields when rendering a form on to the page. On a Server Rendered Application, the server decides what fields to put in the form, and there is no way to add additional fields. That means that processing any amount of variable information requires multiple interactions with the server. For example, if we have a "recipe editor" and we want to add multiple ingredients, adding each ingredient typically requires a trip to the server to submit the new ingredient and render a new form to add more ingredients (while keeping the intermediate state around) On a Blazor Server or Webassembly application this is not a problem since the app can manipulate the DOM dynamically and the state is preserved in memory and not serialized into the form.
Sending form data and processing a formThe most constrained environment is Server Rendered apps where we are limited by what the browser supports. The process for generating a form and processing it goes like this: sequenceDiagram
actor User
participant Browser
participant Server
User->>+Browser: Types URL
Browser->>+Server: GET <<URL>>
deactivate Browser
Server->>-Browser: 200 OK <br><form action="" method="POST"><br><label for="name">Enter your name: </label><br><input type="text" name="name" id="name"><br><br><input type="submit" value="Subscribe!"><br></form>
User->>Browser: Enters name on textbox<br>Clicks Subscribe!
Browser->>+Server: POST <<URL>><br>name="<<name>>
Note right of Server: Route request to handler<br>Read data<br>Validate data<br>Process data/action<br>Generate response
Server->>Browser: 301 Redirect<br>Location:<<new-url>>
deactivate Server
Browser->>+Server: GET <<new-url>>
Server->>-Browser: 200 OK
Browser->>User:
The contract from the server and the browser is as follows:
In order for Blazor to handle forms, we need to establish two mappings:
The app needs to be able to validate the incoming data, and trigger any potential action as a result of the request. That gives us the 4 steps:
These steps are only necessary in passive rendering because Blazor Server can interact with the data directly and Blazor Webassembly typically uses an API call to achieve the same result. Routing to a handlerThe way Blazor normally defines a handler for a form post, is via the Other frameworks like Razor pages define a method on the component that gets called by convention when a POST request arrives. The main differences between the two approaches are:
The common approach for server rendered technologies is that when a request is submitted, an instance of the handler is created, data is bound, and then the app renders a response. It seems clear then, that given the constraints above (limitting forms to pages being the least important). We want to follow a process similar to:
We can choose to have a special method to specifically handle form posts, but it needs to be bound to the form via an attribute that gets populated during render. Otherwise, we end up duplicating logic for validation and so on. If we can live with the It is clear then that we need to establish a mapping between the We can think of compile time generating an identifier for each form that we render, but that will make it impossible to render more than one instance of a component with that form into the page, and won't allow libraries to develop form components. The alternative is to give the form a name that is controlled by the app developer. This could be achieved with the With that, we are able to identify the form, but we still need a way to express that in HTML so that the information can be sent during the POST request to the server. There are a couple of ways in which we can do that:
With all these pieces in place, we can establish a mapping between the client and the server in the following way: <form
+ action="?handler=myHandler"
method="POST"
@onsubmit="OnSubmit"
+ @onsubmit:name="myHandler"
>
...
</form> That is the raw way of achieve the mapping with a plain form, but edit form can do a bit better: <EditForm Name="myHandler" OnValidSubmit="ProcessForm">
</EditForm> which internally translates to <form
+ action="?handler=myHandler"
method="POST"
@onsubmit="OnEditFormSubmit"
+ @onsubmit:name="myHandler"
>
...
</form> We still need to have a way to make these identifiers unique, but we are going to tackle that problem separately. When to bind form dataThe other part that we need to tackle is establishing a mapping between the data in the form and some state on the server. This problem is generally referred to as "model binding". In the common case for a Blazor application, data is populated during the first render of a component, within the OnInitialized(async) method call. We want model binding to happen at that stage, otherwise we force the app to load the data unnecessarily. There are several approaches we can use to define how we bind the data:
Based on this, it is clear that binding needs to happen while the app is rendering, and before we process the action (by triggering the form event) so that all the components that can influence the processing of the data can be instantiated. That leaves us with two options:
Actually, there is a third option that we have not considered. This third option is a Cascading values are a way to provide context to a component "from outside". Typically an ancestor component. This approach is a combination of the two approaches above:
There is one problem though. We don't have an instance of the value to inject, we only have form data, and we would need to process that data before we can set the value. Finally, one aspect that we have not considered but that also applies to binding when multiple forms are rendered, is that we need to selectively bind the data only to the component containing the form that triggered a request. Other forms should just load their data through their regular mechanism without trying to bind it. Since this data is typically loaded within the components, we also need to ask ourselves the question of whether this data is a Parameter or part of the component state. (Can it be seen as an optional parameter with a default value?). How to bind form dataThis is the process by which we establish a mapping between the data sent in the POST request (the form URL encoded data) and the members in the type we want to "bind" the data to. This problem is equivalent to deserializing other formats like JSON or XML, and the same constraints and concerns apply. The main aspect we need to take into account is how we establish the mapping between the form data and the value that we want to populate. There are a set of known of primitive types that get mapped directly (pretty much anything that implements IParseable). Then we have to think about complex types:
And we need to think about collections:
We can choose how complex this mapping can be, as we can always extend it in the future without introducing a breaking change. Since forms only support key value pairs in the form of "name"="value", we are forced to use the "object path" as the name for every leaf primitive value that we want to set. For example, given the following code: public class Customer
{
public Address Address { get; set; }
}
public class Address
{
public string Street;
public string Province;
public string Country;
} The "object paths" in the form would be like:
If we decide to do collections, the syntax that is already supported by MVC and Razor Pages is as follows:
Note that the index can be a number for IEnumerable collection types or a string for dictionary types. When binding collections the Indexes must be consecutive, meaning there can't be gaps between them (must check this). The way we specify these values is via the name attribute on the
Our components can do better though, since most of them support
Writing complex forms split across componentsThe approach described works so far when all the inputs defined in the form are defined explicitly on the same component. However, in complex forms, it is common to split the form definition into multiple subcomponents that get rendered inside the form. In our recipe editor app, this is the case for the ingredients list component. The problem is that at the point we render that component, we loose track of the "prefix" that needs to be used for the name field. This means that the developer needs to pass this prefix explicitly to the component and the component needs to take it into account to preprend it to any other input element rendered inside the child component. In Razor (for MVC and Razor Pages), the ViewData dictionary contains a "TemplateInfo" object that propagates this prefix when rendering elements in partial views, tag helpers, etc. We can choose to support a similar concept in Blazor using a cascading value instead provided by a special component
And inside <InputText @bind-Value="value.Street" /> Which will produce the result: <input name="customer.Address.Street" value="" /> The reason this is needed in server rendered applications and is not needed in Blazor Server or Webassembly is because in the latter two cases, you always maintain "referential integrity" between the input element and the data associated with the form, while on server rendered apps, that's not the case. Hence why we need to use the "name" property in the input element, and why input elements need to be "context aware" instead of relying on the pure bindings. Note that we can choose to not build Form handling a complete exampleWith the two aspects described above, lets look at how a component can potentially look: Recipe.razor @page /Recipe/Edit/{id}
<div>
<h1>Recipe Editor</h1>
<p>Share your great recipes with the Best For You community.</p>
<EditForm Name="EditRecipe" Model="@recipe" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<h2>Title</h2>
<InputText Name="Name" @bind-Value="@recipe.Name" />
<ValidationMessage For="@(() => recipe.Name)" />
<h2>Number of servings</h2>
<InputNumber Name="Servings" @bind-Value="@recipe.Servings" />
<ValidationMessage For="@(() => recipe.Servings)" />
<EditGroup Name="recipe.Ingredients">
<h2>
<IngredientsListEditor @bind-Ingredients="@recipe.Ingredients" />
</h2>
<EditGroup>
<ValidationMessage For="@(() => recipe.Ingredients)" />
<h2>Instructions</h2>
<InputTextArea Name="Instructions" rows="5" name="text" placeholder="Write your instructions" @bind-Value="@recipe.Instructions" />
<ValidationMessage For="@(() => recipe.Instructions)" />
<button type="submit">Submit recipe</button>
</EditForm>
</div>
@code {
[Parameter] public string Id { get; set; }
// Option 1: Requires some component or runtime support to bind the value
[SupplyFromBody] public Recipe Recipe { get; set; }
// Option 2: Requires framework integration
[CascadingParameter(Source=FromBody)] public Recipe Recipe { get; set; }
public void OnIntializedAsync()
{
Recipe ??= await Repository.LoadRecipe(Id);
}
async Task HandleSubmit()
{
recipe.Tags = new[] { "TODO" };
recipe.Id = await RecipesStore.UpdateRecipe(recipe);
Nav.NavigateTo($"recipe/{recipe.Id}");
}
} Disambiguating formsWhen there is more than one form on the page, we need to decide what part of the application gets to bind data and what form gets to handle the event. For forms, the event will come to a url with a query string parameter For the property the form needs to bind to, there is no association that maps a concrete handler to a property. However, we can use the component hierarchy to disambiguate. For example, imagine we have an app, where there are two forms. One on the main page, and another one in a side bar menu. We can give each "section/area" a name, like "main" and "navigation". We can cascade those names down the component hierarchy. Components can consume those names and use them for uniquely identifying data to bind to and form handlers. This removes the need for our forms to explicitly use a name when they are inside a section. If we put all the components on a single file it would look like this: <Section Name="navigation">
<!-- Imagine this is inside NavMenu.Razor -->
<!-- Imagine there is an EditMenuModel property defined with the right attributes to trigger binding -->
<EditForm Model="EditMenuModel" OnValidSubmit="NavMenuSubmit">
...
</EditForm>
</Section>
<Section Name="main">
<!-- Imagine this is inside Index.Razor or other page -->
<!-- Imagine there is an CustomerModel property defined with the right attributes to trigger binding -->
<EditForm Model="CustomerModel" OnValidSubmit="AddCustomer">
...
</EditForm>
</Section> This would translate to: <!-- Content inside NavMenu.razor -->
<form action=?handler="navigation" method="POST">
...
</form>
<!-- Content inside Index.razor -->
<form action=?handler="main" method="POST">
...
</form> With this in mind, we have the final mapping that we needed, which is the "handler" is associated with the property to be "bound" to, via the scope. There are two important considerations here:
Streaming renderingWhen a component opts-in for streaming rendering, we don't expect the streaming to start until the first render happens after we've dispatched the event. The application needs to achieve quiescence before we dispatch the event, to ensure that the form component has been rendered and any required value has been bound. Once we invoke the handler, we want to start the streaming process after the dispatched event produces a render. This allows the app to display intermediate states while processing the form. It can for example, disable the form during the processing of the event, and it can display progress. There are considerations to be taken into account with regards to antiforgery, see the details on "Security considerations". RedirectingIt is going to be relatively common that we need to redirect the browser after successfully handling an action. We could do this in two ways:
Error handlingThere are several classes of errors that are unique to the way Server Rendered apps work. Server rendered apps introduce new failure modes in the form of:
These types of errors are in many cases an application error or the result of an exceptional situation. We need to have a way to express these concepts in server rendered applications. There are several ways in which we can do this:
Some security constraints need to be validated before we try to process the form, like the form limits or antiforgery, as a result these need to be enforced before form is even rendered and might need to result in a "hard failure". Meaning that we don't even render the page, and we send back a 400. For determining whether the model was correctly bound, the approach MVC uses is to avoid producing an error in the first place, and force the user to check the ModelState in the controller page. The equivalents in Components is the EditContext, but this only gets populated after we have rendered the form at least once. If we use a component, the app has no way within Security considerationsThe security checks need to be performed in a specific order. For example, form limits need to be applied before we try to validate an antiforgery tokens. We need to be able to define form limits on a "per form" basis, as the global default might not work for a specific example. It is important to note that a well functioning app should never fail because the form limits have been exceeded. There should be validation rules on the app to prevent this siutation. AntiforgeryAntiforgery and streaming rendering need to work together. During get requests, the antiforgery token needs to be appended as a cookie to the response. Regularly, the antiforgery token needs to be updated (to refresh it) or because the user identity changed as part of a login. We can choose to always attach the antiforgery token to the response before start streaming any response.
We can choose to have a way to signal this requirement on a per endpoint level.
We can do nothing and throw:
Modelbinding limitsSince we are binding data on the server, we need to account for limits in terms of how much "recursion" we are willing to do (how much we want to dive into nested structures to perform deserialization) and how big can the collections we are binding be. See here for more details Open questions
|
SummaryProvide support for handling server side rendered forms within Razor Component Applications as part of "Blazor United". Motivation and goalsBlazor united apps provide a unified model for authoring web applications whether the app is server rendered or a SPA. Handling form data is part of such experience. The only way customers have to provide a "server rendered only" experience with components requires using MVC or Razor Pages. We want to make it easier for customers to reach a wider customer base with the same code.
In scope
Out of scope
Risks / unknownsCovered (hopefully) in the big doc. Detailed designScenario 1@page /Recipes/New
@inject NavigationManager Navigation;
@inject RecipeRepository Recipes;
<div>
<EditForm Model="@NewRecipe" OnValidSubmit="AddRecipe">
<DataAnnotationsValidator />
<AntiforgeryToken />
<ValidationSummary />
<h2>Title</h2>
<InputText @bind-Value="@NewRecipe.Name" />
<ValidationMessage For="@(() => NewRecipe.Name)" />
<h2>Number of servings</h2>
<InputNumber @bind-Value="@NewRecipe.Servings" />
<ValidationMessage For="@(() => NewRecipe.Servings)" />
<h2>Instructions</h2>
<InputTextArea rows="5" name="text" placeholder="Write your instructions" @bind-Value="@NewRecipe.Instructions" />
<ValidationMessage For="@(() => NewRecipe.Instructions)" />
<button type="submit">Submit recipe</button>
</EditForm>
</div>
@code {
[SupplyFromForm] public Recipe NewRecipe { get; set; }
public override OnInitialized() => NewRecipe ??= new Recipe();
public async Task AddRecipe(SubmitEvent evt)
{
await Recipes.Add(NewRecipe);
Navigator.NavigateTo($"/Recipes/{Recipe.Id}");
}
} Steps:
Work items
Scenario 2
RemoveIngredient.razor <EditForm Model="@Ingredient" OnValidSubmit="RaiseOnRemoveIngredient">
<!-- Renders <form action=?handler="<<scope-chain>>" ...> -->
<DataAnnotationsValidator />
<AntiforgeryToken />
<ValidationSummary />
<InputHidden Value="@Ingredient.Id">
<button type="submit">Remove ingredient</button>
</EditForm>
@code
{
[Parameter] public Ingredient Ingredient { get; set; }
[Parameter] public EventCallback<int> OnRemoveIngredient { get; set; }
[SupplyValueFromForm] public int Id { get; set; }
public Task RaiseOnRemoveIngredient(){
return OnRemoveIngredient.InvokeAsync(Id);
}
} Steps:
Scenario 3<div>
<EditForm
+ Handler="@nameof(EditRecipe)
Model="@Recipe"
OnValidSubmit="EditRecipe">
<DataAnnotationsValidator />
<AntiforgeryToken />
<ValidationSummary />
<h2>Title</h2>
<InputText @bind-Value="@Recipe.Name" />
<ValidationMessage For="@(() => Recipe.Name)" />
<h2>Number of servings</h2>
<InputNumber @bind-Value="@Recipe.Servings" />
<ValidationMessage For="@(() => Recipe.Servings)" />
<h2>Instructions</h2>
<InputTextArea rows="5" name="text" placeholder="Write your instructions" @bind-Value="@Recipe.Instructions" />
<ValidationMessage For="@(() => Recipe.Instructions)" />
<button type="submit">Submit recipe</button>
</EditForm>
</div>
+<!-- Generates <form action="?handler="EditRecipe"> -->
@code {
+ [SupplyFromForm(nameof(EditRecipe))]
public Recipe Recipe { get; set; }
[Parameter] public int Id { get; set; }
public async Task override OnInitializedAsync() => Recipe ??= await Recipes.GetById(Id);
public async Task EditRecipe(SubmitEvent evt)
{
Recipe.Id = Id;
await Recipes.Update(Recipe);
Navigator.NavigateTo($"/Recipes/{Recipe.Id}");
}
} Workitems
|
Is this a new type, or something from MVC model binding? I'm guessing new type for layering.
Could you rephrase? I don't know what this remark is about at all.
If possible let's explore if we can make this be supplied as an
We may be able to avoid having a new renderer API for this. The prerenderer already gets RenderBatch instances with the event handler info, so it can search those to find the
Sounds reasonable that
I'm not sure how the endpoint knows whether or not you have antiforgery validation enabled. Should the enforcement perhaps be inside the
I'm not sure we need to wait for quiescence. In almost all cases the form will be rendered synchronously anyway, and if there's some completely unrelated component elsewhere in the hierarchy that's still loading stuff, why would we want to wait for that? Would anything bad happen if we don't wait for quiescence?
Which code actually is responsible for dispatching this event? Presumably something in the passive rendering logic. Is that right?
I think that can be regarded as unrelated to this work. It's a feature we need independently of form post handling.
If at all possible I'd love to avoid introducing the
I think we need a more specific name - handler could mean anything. Maybe
If it's a form with Also should we have an error if more than one form matches the specified handler/name? That would catch issues where otherwise you might not notice that multiple |
BTW sorry that sounds like a long list of objections/complaints! I think this is good design work and am only posting all this stuff as a big block to get the risk areas out of the way early and hopefully leave you as unblocked as possible as soon as possible. |
Yes, this is a new type defined for Components. I am hoping that by default this can be kept "hidden" unless you explicitly ask for it via a
Update component base to bind annotated properties for a given component before OnInitialized(Async) runs. If possible let's explore if we can make this be supplied as an ICascadingValueComponent so that it naturally works through the existing cascading value system, and no new implementation is needed on ComponentBase, and it would even work with pure IComponent. Update EditForm to register a handler for the submit event of the form element with the renderer (via a new RenderTreeBuilder API). Yes, my idea is that this goes through
Registering the event name is for performance, to avoid the search through the render batch. I expect us to be able to do this pretty efficiently.
This is the idea, EditForm retrieves the BindingContext and just calls a method to populate the EditContext with it values. EditForm just "orchestrates".
The enforcement needs to happen before model binding happens. We have some options described above, like making the check always within a POST request, and in addition we can have an attribute that you apply to the thing you are binding to optionally skip it (but should be on by default). Based on experience with Razor Pages, having antiforgery on by default has not been an issue and it's a much safer default.
Waiting for quiescence is the best guarantee we have that the app is in as close state as it was when it rendered during the initial request. While it might be true that the form will render synchronously most of the time, we can't be sure, so it is safer to wait by default and maybe provide an option. As an alternative, we can register the named event during the initial call to start the render, and the renderer can then automatically fire it the moment it sees a component define the named event. That actually makes it even better, because it means that we don't need to keep track of named events. We can just have one slot for the event we are processing on the renderer and the moment we see it defined, we can queue a call to dispatch it.
Yes, the endpoint triggers the dispatch of the named event via the passive renderer.
Yep, I am aware, just putting it here so that we don't lose track of it.
I chose "scope" just to be clear about what it does, but this idea comes from the improved persistence in the prototype. Essentially, it's the same as the "PersistentBoundary" here when/if we get to that bit, I want that concept to also be usable here to disambiguate. This is also done as a component, but we can potentially find a different way to do it that does not involve an extra component. It's just more work. For example, it could be a directive `@scope="@(<>)" that we convert into a "special" value that we can understand and cascade through the rest of the hierarchy. Hope that makes sense
I used Handler just to mimic the razor pages term. We can use
Hidden has problems. It's not visible to other parts of the stack without reading the body, which is generally frowned upon, as you want to do security validation before you read the body. I can sympathize with the handler appearing in the UI, but the reality is that it's the only thing that works with both GET and POST forms and that can "safely" be read by other parts of the stack. We can't use the path because otherwise people can't use catch-all parameters on their routes, and we want the handler being invoked to be clearly identifiable in the server logs and other things like that, that won't read the body. Once we do the "enhanced mode" we can put the handler in a custom request header, and that way we avoid the UI problem. Would that work? (The thing that processes the "enhanced" form just strips the query parameter from the action and send it on the request header instead)
Yes, the event name should be unique across the entire component hierarchy. We can probably validate this if we see more than one component try to register the same name during the render as part of the "POST" request. At which point we can choose to throw an exception. |
Can we clarify here in the proposal what gets rendered in the form for the action value by default? |
The "we" here makes it sound like the framework will do this, but this is just user code, correct? |
Form binding for Blazor SSR appsThe goal is to enable Blazor SSR apps to handle forms in a natural way similar to how Razor Pages and MVC apps do so. Upon receiving a POST request, we identify what area of the page (if any) is requesting the data to be bound as a parameter. A user specifies this by using the [SupplyParameterFromForm] attribute on a property in the component. From there on, we combine Who gets to do bindingThere is only one property in the entire component hierarchy that gets to bind data using Form inside a page +<EditForm Model="Customer" OnValidSubmit="AddCustomer" novalidate>
<DataAnnotationsValidator />
<div>
<ValidationSummary />
</div>
<div>
<label for="Name">Name</label>
<InputText @bind-Value="Customer.Name" />
<ValidationMessage For="() => Customer.Name" />
</div>
<div>
<label for="Age">Age</label>
<input type="text" name="Age" value="@Customer.Age" />
<ValidationMessage For="() => Customer.Age" />
</div>
<div>
<input type="submit" value="Add Customer" />
</div>
</EditForm>
@code {
private bool _customerAdded;
+ [SupplyParameterFromForm] public CustomerDto Customer { get; set; }
} Form with a name <EditForm
+ FormHandlerName="Handler"
Model="Customer" OnValidSubmit="AddCustomer" novalidate>
...
</EditForm>
@code {
+ [SupplyParameterFromForm(Name="Handler")] public CustomerDto Customer { get; set; }
}
Binding conventionsAs we said, there is only one property in the component hierarchy that gets to bind data. There are two cases that we care about when binding properties:
For example, for a Customer type with an Address property, the paths are represented as This convention is intentionally compatible with the MVC and Razor Pages convention to ease migration scenarios and support using component forms in a natural way inside Razor Pages and MVC. Binding processThere are 4 cases we care about:
Single valuesAny type that wants to be treated as a single atomic value for the purposes of binding, needs to implement All common runtime types are covered by this. Some examples are: Any type that is not considered directly parsable from a single value, is considered either a collection, dictionary, or object. CollectionsWe support binding to a set of well-known collection types, interfaces and arrays, as well as any concrete type that implements ICollection. The set of well-known types and interfaces is: All other concrete types in System.Collection.Generics like Collection types are bound starting from 0 to the last element, and there can't be any gaps in the sequence. We simply stop binding values the moment we don't find an index. We do this for two reasons:
Values for collections are specified using the square brackets syntax, so [0], [1], etc. and compose with other elements (for example in complex objects as DictionariesWe support binding to a set of well-known dictionary types, interfaces and arrays, as well as any concrete type that implements IDictionary<TKey, TValue>. The set of well-known types and interfaces is: All other concrete types, fall in the general category of types that implement If a type implements When binding dictionary types, we don't know the set of keys ahead of time without iterating over all the fields in the form. This means that we can't use the same approach we use for collections, so we delay binding dictionaries as much as possible, as it forces us to compute the set of keys for the dictionary to be able to bind the values. We do this by iterating over the set of keys and constructing a tree where each node has a key value, and each branch is tagged with the prefix for that key. Dictionary types are bound using the square brackets syntax, so [key], [key1], [key2], etc. and compose with other elements (for example in complex objects as Complex typesOur approach for binding complext objects is to iterate over all the properties in the object, append the property name to the value we are trying to bind as a prefix, and then recursively try to bind the value. For example, if we are trying to bind a value to We only create an instance for a type if we find at least one value for it. For example, we won't create an instance of Address unless we find a value for either street, city, country, etc. When we have at least one value, we create a new instance using the parameterless constructor, and we set the values for the properties we found. We support binding to any type that has a public parameterless constructor and public writable properties. We don't support binding to fields, or properties with private setters. There are some features supported in other serializers that we won't support. In general, this aligns with with the support provided by Razor Pages and MVC. The set of features that we do not support are:
There are other set of features that we should support but that we haven't set on a strategy. These are:
There are several approaches we can take here:
In general, while these are convenient features, there is always the option to create a DTO whose shape matches exactly the fields that you are expecting to receive from the form, and then map the DTO to the actual model. The most risk averse approach is to support [DataContract] and [DataMember] attributes, and then optionally introduce new attributes if we need them. We could also consider supporting customization only via source generation. That "frees" us from having to take concrete dependencies in the framework and would allow us to respect attributes that we would not consider due to layering, like [BindNever] or [BindRequired] from MVC. ExtensibilityRight now we haven't built any extensibility mechanism into the system, but we can think that such a similar mechanism like the one MVC and Razor Pages has or System.Text.Json is needed. Particularly the ability to specify a custom binder for a given type, or property within a type, as well as the ability to create a factory that can handle a variable number of types. The one thing that we won't support is the ability to customize the binding process on a per request basis. That adds an unnecessary level of complexity to the pipeline and precludes us from performing critical optimizations. General binding algorithmThe general algorithm for binding is as follows:
In general, we expect that the forms that are sent from the browser contain values for the properties that we are trying to bind. We don't try to compute the set of known prefixes ahead of time. That requires us to process the form keys and store the result of that process. Instead, we do that processing inline, by iterating over the keys and computing the set of prefixes as we go. This means for example, that for an object, like a Customer with an Address, we only perform a lookup for a fixed set of values on the form, like For collections is similar, because even if there are multiple elements, we can precompute the indexes, and we visit them in order. The only case where we are forced to inspect the entire set of form values is when dealing with dictionaries, but that's a much rarer case than the other two. Implementation detailsRight now we are using the form collection wrapped in an There are a few optimizations that we can apply when we are processing the form data stream if we are able to process the underlying form data stream directly. These optimizations present variable levels of complexity:
The binding process itself doesn't allocate more than what is strictly necessary, as the only allocations that we do are for the instances that we create for the types that we are binding to. The entire binding process is typed, so value types are never boxed. |
Does this mean people could never do something like the following: @code {
[SuplyParameterFromForm] public string UserName { get; set; }
[SuplyParameterFromForm] public string Password { get; set; }
} i.e., does the design imply that people must always create a single DTO representing the shape of the incoming form post? I'm not sure whether this point in the design only refers to one scenario out of many, or whether it's saying this is literally the only supported scenario. |
@SteveSandersonMS Yep, only one parameter gets to bind the form. Minimal I believe follows the same approach. This is different from MVC that would bind multiple separate values from the form. |
OK. How close are we to being able to do some app building to explore all of this in practical scenarios? I'm aware of some pending PRs related to model binding particular types/collections/etc (e.g., #48559). Are they what's needed for us to try it out, or are there still other parts of the internals like handling POST requests that are unrelated to an |
The two remaining missing parts are validation/error handling and antiforgery, once the bindings PRs get through.
That's tracked by #47804 |
It does not. Minimal APIs form binding allows binding multiple parameters in .NET 8 (simple values though). |
I thought for complex values only one thing reads the form (like JSON) |
Yes, for complex values, you only get one (like JSON). |
@davidfowl ok, so we will do the same? (Complex values you get one, simple values, you get any you want) |
Is there some reason why that restriction is desirable? Wouldn't it be preferable for people just to be able to bind as many complex objects as they want? For example if a complex form is split up over many components. I know people could make a DTO representing the whole top-level thing, but what makes us want to force them to do that? |
How will that look like?
It's a scoping decision. We can always add binding multiple complex values later, but we don't have to worry about modeling all those scenarios for 8.0 |
@javiercn I think this issue might have reached the end of its useful life since it now describes a huge amount of stuff and many different iterations of the design. Much of it is covered by other issues now (e.g., antiforgery, error handling). The issue title is also very generic and overlaps with lots of other issues, so to bring clarity, it would be good to close this. Would you be OK with me closing this and creating a new issue for "Blazor form post model binding" and copying in the list of things from this issue description that appear to refer to model binding? |
Actually since I'm tidying up now I'm just going to go ahead and close this in favour of #48759. If that's the wrong thing to do please let me know. |
This needs design.
Mapping form data specifically.
The text was updated successfully, but these errors were encountered: