-
Notifications
You must be signed in to change notification settings - Fork 10.3k
[Blazor] Adds support for server side rendered forms (without a body) #47716
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
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
00d7325
Enable handling POST requests
javiercn bac4bf5
Add DebuggerDisplay to component state
javiercn 55922e7
Set form name
javiercn ed7c793
Track named event handlers
javiercn 0c9bb3f
Add tests for tracking named event handlers in EndpointHtmlRenderer
javiercn 9ab5011
Define CascadingModelBinder and ModelBindingContext
javiercn 44d4e2a
Add specialized RenderFragment AddComponentParameter overload and rem…
javiercn 9c59e87
Add CascadingModelBinder to RouteView
javiercn 3906d9c
Dispatch forms with streaming rendering
javiercn 4dd9726
More tests
javiercn b6e4c48
Add E2E test
javiercn 52001ca
Support named forms and hierarchical handlers
javiercn 32f0f4c
Fix tests
javiercn dbac6ce
Cleanups and fix tests
javiercn 571b1c3
Add additional route view tests
javiercn 4d315ee
CascadingModelBinder, ModelBindingContext and RouteView test
javiercn 37f1a67
CascadingModelBinder updates
javiercn 9b6dc08
Fix tests
javiercn 6d3da0e
Rework cascadingmodelbinder to get rid of BindingContextId
javiercn 9921089
Cascading model binder updates
javiercn e8ba09f
E2E tests
javiercn d67a5c4
Update cascading model binder and tests
javiercn 94e3702
Add E2E tests
javiercn be14184
Cleanup empty lines
javiercn 7d72205
Another test
javiercn 298c178
Additional cleanups
javiercn 5415283
Fix DispatchEvent
javiercn ed05d7e
Addressed feedback from Mackinnon
javiercn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
src/Components/Components/src/Binding/CascadingModelBinder.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Reflection.Metadata; | ||
using Microsoft.AspNetCore.Components.Binding; | ||
using Microsoft.AspNetCore.Components.Routing; | ||
|
||
namespace Microsoft.AspNetCore.Components; | ||
|
||
/// <summary> | ||
/// Defines the binding context for data bound from external sources. | ||
/// </summary> | ||
public sealed class CascadingModelBinder : IComponent, IDisposable | ||
{ | ||
private RenderHandle _handle; | ||
private ModelBindingContext? _bindingContext; | ||
private bool _hasPendingQueuedRender; | ||
|
||
/// <summary> | ||
/// The binding context name. | ||
/// </summary> | ||
[Parameter] public string Name { get; set; } = ""; | ||
|
||
/// <summary> | ||
/// If true, indicates that <see cref="ModelBindingContext.BindingContextId"/> will not change. | ||
/// This is a performance optimization that allows the framework to skip setting up | ||
/// change notifications. Set this flag only if you will not change | ||
/// <see cref="Name"/> of this context or its parents' context during the component's lifetime. | ||
/// </summary> | ||
[Parameter] public bool IsFixed { get; set; } | ||
|
||
/// <summary> | ||
/// Specifies the content to be rendered inside this <see cref="CascadingModelBinder"/>. | ||
/// </summary> | ||
[Parameter] public RenderFragment<ModelBindingContext> ChildContent { get; set; } = default!; | ||
|
||
[CascadingParameter] ModelBindingContext? ParentContext { get; set; } | ||
|
||
[Inject] private NavigationManager Navigation { get; set; } = null!; | ||
|
||
void IComponent.Attach(RenderHandle renderHandle) | ||
{ | ||
_handle = renderHandle; | ||
} | ||
|
||
Task IComponent.SetParametersAsync(ParameterView parameters) | ||
{ | ||
if (_bindingContext == null) | ||
{ | ||
// First render | ||
Navigation.LocationChanged += HandleLocationChanged; | ||
} | ||
|
||
parameters.SetParameterProperties(this); | ||
if (ParentContext != null && string.IsNullOrEmpty(Name)) | ||
{ | ||
throw new InvalidOperationException($"Nested binding contexts must define a Name. (Parent context) = '{ParentContext.Name}'."); | ||
} | ||
|
||
UpdateBindingInformation(Navigation.Uri); | ||
Render(); | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
private void Render() | ||
{ | ||
if (_hasPendingQueuedRender) | ||
{ | ||
return; | ||
} | ||
_hasPendingQueuedRender = true; | ||
_handle.Render(builder => | ||
{ | ||
_hasPendingQueuedRender = false; | ||
builder.OpenComponent<CascadingValue<ModelBindingContext>>(0); | ||
builder.AddComponentParameter(1, nameof(CascadingValue<ModelBindingContext>.IsFixed), IsFixed); | ||
builder.AddComponentParameter(2, nameof(CascadingValue<ModelBindingContext>.Value), _bindingContext); | ||
builder.AddComponentParameter(3, nameof(CascadingValue<ModelBindingContext>.ChildContent), ChildContent?.Invoke(_bindingContext!)); | ||
builder.CloseComponent(); | ||
}); | ||
} | ||
|
||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) | ||
{ | ||
var url = e.Location; | ||
UpdateBindingInformation(url); | ||
Render(); | ||
} | ||
|
||
private void UpdateBindingInformation(string url) | ||
{ | ||
// BindingContextId: action parameter used to define the handler | ||
// Name: form name and context used to bind | ||
// Cases: | ||
// 1) No name ("") | ||
// Name = ""; | ||
// BindingContextId = ""; | ||
// <form name="" action="" /> | ||
// 2) Name provided | ||
// Name = "my-handler"; | ||
// BindingContextId = <<base-relative-uri>>((<<existing-query>>&)|?)handler=my-handler | ||
// <form name="my-handler" action="relative/path?existing=value&handler=my-handler | ||
// 3) Parent has a name "parent-name" | ||
// Name = "parent-name.my-handler"; | ||
// BindingContextId = <<base-relative-uri>>((<<existing-query>>&)|?)handler=my-handler | ||
var name = string.IsNullOrEmpty(ParentContext?.Name) ? Name : $"{ParentContext.Name}.{Name}"; | ||
var bindingId = string.IsNullOrEmpty(name) ? "" : GenerateBindingContextId(name); | ||
|
||
var bindingContext = _bindingContext != null && | ||
string.Equals(_bindingContext.Name, Name, StringComparison.Ordinal) && | ||
string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal) ? | ||
_bindingContext : new ModelBindingContext(name, bindingId); | ||
|
||
// It doesn't matter that we don't check IsFixed, since the CascadingValue we are setting up will throw if the app changes. | ||
if (IsFixed && _bindingContext != null && _bindingContext != bindingContext) | ||
{ | ||
// Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized | ||
// as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations: | ||
// * Component ParentContext hierarchy changes. | ||
// * Technically, the component won't be retained in this case and will be destroyed instead. | ||
// * A parent changes Name. | ||
throw new InvalidOperationException($"'{nameof(CascadingModelBinder)}' 'Name' can't change after initialized."); | ||
} | ||
|
||
_bindingContext = bindingContext; | ||
|
||
string GenerateBindingContextId(string name) | ||
{ | ||
var bindingId = Navigation.ToBaseRelativePath(Navigation.GetUriWithQueryParameter("handler", name)); | ||
var hashIndex = bindingId.IndexOf('#'); | ||
return hashIndex == -1 ? bindingId : new string(bindingId.AsSpan(0, hashIndex)); | ||
} | ||
} | ||
|
||
void IDisposable.Dispose() | ||
{ | ||
Navigation.LocationChanged -= HandleLocationChanged; | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
src/Components/Components/src/Binding/ModelBindingContext.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace Microsoft.AspNetCore.Components.Binding; | ||
|
||
/// <summary> | ||
/// The binding context associated with a given model binding operation. | ||
/// </summary> | ||
public sealed class ModelBindingContext | ||
{ | ||
internal ModelBindingContext(string name, string bindingContextId) | ||
{ | ||
ArgumentNullException.ThrowIfNull(name); | ||
ArgumentNullException.ThrowIfNull(bindingContextId); | ||
// We are initializing the root context, that can be a "named" root context, or the default context. | ||
// A named root context only provides a name, and that acts as the BindingId | ||
// A "default" root context does not provide a name, and instead it provides an explicit Binding ID. | ||
// The explicit binding ID matches that of the default handler, which is the URL Path. | ||
if (string.IsNullOrEmpty(name) ^ string.IsNullOrEmpty(bindingContextId)) | ||
{ | ||
throw new InvalidOperationException("A root binding context needs to provide a name and explicit binding context id or none."); | ||
} | ||
|
||
Name = name; | ||
BindingContextId = bindingContextId ?? name; | ||
} | ||
|
||
/// <summary> | ||
/// The context name. | ||
/// </summary> | ||
public string Name { get; } | ||
|
||
/// <summary> | ||
/// The computed identifier used to determine what parts of the app can bind data. | ||
/// </summary> | ||
public string BindingContextId { get; } | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to avoid allocations altogether if we are not interesting on tracking event handlers. We only track them at the time we want to dispatch an event.We actually want to avoid tracking events altogether when we are not trying to dispatch an event. That limits/eliminates any chance we introduce breaking changes if for example, someone have today multiple forms defined on their app without giving them unique event handler names.