Skip to content

Commit c69cb31

Browse files
Forms PRG and error handling fixes (#49472)
* Add some E2E test app code that demonstrates problems (no actual E2E test scripts yet though) * Fix one of the problems (streaming SSR wasn't returning synchronous errors) * Fix event dispatch error handling * E2E cases * Fix and test for P/R/G * Renames from other PR review
1 parent e5bb36d commit c69cb31

17 files changed

+354
-59
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionE
102102
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask
103103
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
104104
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!
105-
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task!
105+
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool waitForQuiescence) -> System.Threading.Tasks.Task!
106106
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ResolveComponentForRenderMode(System.Type! componentType, int? parentComponentId, Microsoft.AspNetCore.Components.IComponentActivator! componentActivator, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> Microsoft.AspNetCore.Components.IComponent!
107107
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode
108108
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.NamedEventAssignedName.get -> string

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ protected virtual ComponentState CreateComponentState(int componentId, IComponen
380380
/// </returns>
381381
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs)
382382
{
383-
return DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs, quiesce: false);
383+
return DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs, waitForQuiescence: false);
384384
}
385385

386386
/// <summary>
@@ -389,15 +389,20 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
389389
/// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
390390
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
391391
/// <param name="fieldInfo">Information that the renderer can use to update the state of the existing render tree to match the UI.</param>
392-
/// <param name="quiesce">Whether to wait for quiescence or not.</param>
392+
/// <param name="waitForQuiescence">A flag indicating whether to wait for quiescence.</param>
393393
/// <returns>
394394
/// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
395395
/// has completed.
396396
/// </returns>
397-
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs, bool quiesce)
397+
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs, bool waitForQuiescence)
398398
{
399399
Dispatcher.AssertAccess();
400400

401+
if (waitForQuiescence)
402+
{
403+
_pendingTasks ??= new();
404+
}
405+
401406
var callback = GetRequiredEventCallback(eventHandlerId);
402407
Log.HandlingEvent(_logger, eventHandlerId, eventArgs);
403408

@@ -425,22 +430,9 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
425430
_isBatchInProgress = true;
426431

427432
task = callback.InvokeAsync(eventArgs);
428-
if (quiesce)
429-
{
430-
// If we are waiting for quiescence, the quiescence task will capture any async exception.
431-
// If the exception is thrown synchronously, we just want it to flow to the callers, and
432-
// not go through the ErrorBoundary.
433-
_pendingTasks ??= new();
434-
AddPendingTask(receiverComponentState, task);
435-
}
436433
}
437434
catch (Exception e)
438435
{
439-
if (quiesce)
440-
{
441-
// Exception filters are not AoT friendly.
442-
throw;
443-
}
444436
HandleExceptionViaErrorBoundary(e, receiverComponentState);
445437
return Task.CompletedTask;
446438
}
@@ -453,15 +445,19 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
453445
ProcessPendingRender();
454446
}
455447

456-
if (quiesce)
448+
// Task completed synchronously or is still running. We already processed all of the rendering
449+
// work that was queued so let our error handler deal with it.
450+
var errorHandledTask = GetErrorHandledTask(task, receiverComponentState);
451+
452+
if (waitForQuiescence)
457453
{
454+
AddPendingTask(receiverComponentState, errorHandledTask);
458455
return WaitForQuiescence();
459456
}
460-
461-
// Task completed synchronously or is still running. We already processed all of the rendering
462-
// work that was queued so let our error handler deal with it.
463-
var result = GetErrorHandledTask(task, receiverComponentState);
464-
return result;
457+
else
458+
{
459+
return errorHandledTask;
460+
}
465461
}
466462

467463
/// <summary>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private static bool MatchesScope(string incomingScopeQualifiedFormName, string c
7373
public void Map(FormValueMappingContext context)
7474
{
7575
// This will func to a proper binder
76-
if (!CanMap(context.ValueType, context.MappingScopeName, context.RestrictToFormName))
76+
if (!CanMap(context.ValueType, context.AcceptMappingScopeName, context.AcceptFormName))
7777
{
7878
context.SetResult(null);
7979
}

src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,29 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync(
7777
ParameterView.Empty,
7878
waitForQuiescence: isPost);
7979

80-
var isBadRequest = false;
81-
var quiesceTask = isPost ? _renderer.DispatchSubmitEventAsync(handler, out isBadRequest) : htmlContent.QuiescenceTask;
82-
if (isBadRequest)
80+
Task quiesceTask;
81+
if (!isPost)
8382
{
84-
return;
83+
quiesceTask = htmlContent.QuiescenceTask;
8584
}
86-
87-
if (isPost)
85+
else
8886
{
89-
await Task.WhenAll(_renderer.NonStreamingPendingTasks);
87+
try
88+
{
89+
var isBadRequest = false;
90+
quiesceTask = _renderer.DispatchSubmitEventAsync(handler, out isBadRequest);
91+
if (isBadRequest)
92+
{
93+
return;
94+
}
95+
96+
await Task.WhenAll(_renderer.NonStreamingPendingTasks);
97+
}
98+
catch (NavigationException ex)
99+
{
100+
await EndpointHtmlRenderer.HandleNavigationException(_context, ex);
101+
quiesceTask = Task.CompletedTask;
102+
}
90103
}
91104

92105
// Importantly, we must not yield this thread (which holds exclusive access to the renderer sync context)
@@ -95,7 +108,7 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync(
95108
// renderer sync context and cause a batch that would get missed.
96109
htmlContent.WriteTo(writer, HtmlEncoder.Default); // Don't use WriteToAsync, as per the comment above
97110

98-
if (!quiesceTask.IsCompleted)
111+
if (!quiesceTask.IsCompletedSuccessfully)
99112
{
100113
await _renderer.SendStreamingUpdatesAsync(_context, quiesceTask, writer);
101114
}

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal Task DispatchSubmitEventAsync(string? handlerName, out bool isBadReques
4848
var frameLocation = locationsForName.Single();
4949
var eventHandlerId = FindEventHandlerIdForNamedEvent("onsubmit", frameLocation.ComponentId, frameLocation.FrameIndex);
5050
return eventHandlerId.HasValue
51-
? DispatchEventAsync(eventHandlerId.Value, null, EventArgs.Empty, quiesce: true)
51+
? DispatchEventAsync(eventHandlerId.Value, null, EventArgs.Empty, waitForQuiescence: true)
5252
: Task.CompletedTask;
5353
}
5454

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedCompone
133133
}
134134
}
135135

136-
private static ValueTask<PrerenderedComponentHtmlContent> HandleNavigationException(HttpContext httpContext, NavigationException navigationException)
136+
public static ValueTask<PrerenderedComponentHtmlContent> HandleNavigationException(HttpContext httpContext, NavigationException navigationException)
137137
{
138138
if (httpContext.Response.HasStarted)
139139
{

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,31 @@ public class FormValueMappingContext
1313
/// <summary>
1414
/// Initializes a new instance of <see cref="FormValueMappingContext"/>.
1515
/// </summary>
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>
16+
/// <param name="acceptMappingScopeName">The name of a <see cref="FormMappingScope"/>. Values will only be mapped if the incoming data corresponds to this scope name.</param>
17+
/// <param name="acceptFormName">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>
1818
/// <param name="valueType">The <see cref="Type"/> of the value to map.</param>
1919
/// <param name="parameterName">The name of the parameter to map data to.</param>
20-
public FormValueMappingContext(string mappingScopeName, string? restrictToFormName, Type valueType, string parameterName)
20+
public FormValueMappingContext(string acceptMappingScopeName, string? acceptFormName, Type valueType, string parameterName)
2121
{
22-
ArgumentNullException.ThrowIfNull(mappingScopeName, nameof(mappingScopeName));
22+
ArgumentNullException.ThrowIfNull(acceptMappingScopeName, nameof(acceptMappingScopeName));
2323
ArgumentNullException.ThrowIfNull(valueType, nameof(valueType));
2424
ArgumentNullException.ThrowIfNull(parameterName, nameof(parameterName));
2525

26-
MappingScopeName = mappingScopeName;
27-
RestrictToFormName = restrictToFormName;
26+
AcceptMappingScopeName = acceptMappingScopeName;
27+
AcceptFormName = acceptFormName;
2828
ParameterName = parameterName;
2929
ValueType = valueType;
3030
}
3131

3232
/// <summary>
33-
/// Gets the name of the current <see cref="FormMappingScope"/>.
33+
/// Gets the name of <see cref="FormMappingScope"/> that is allowed to supply data in this context.
3434
/// </summary>
35-
public string MappingScopeName { get; }
35+
public string AcceptMappingScopeName { get; }
3636

3737
/// <summary>
3838
/// 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.
3939
/// </summary>
40-
public string? RestrictToFormName { get; }
40+
public string? AcceptFormName { get; }
4141

4242
/// <summary>
4343
/// Gets the name of the parameter to map data to.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ 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.FormValueMappingContext(string! mappingScopeName, string? restrictToFormName, System.Type! valueType, string! parameterName) -> void
45+
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.AcceptFormName.get -> string?
46+
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.AcceptMappingScopeName.get -> string!
47+
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.FormValueMappingContext(string! acceptMappingScopeName, string? acceptFormName, System.Type! valueType, string! parameterName) -> void
4648
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.MapErrorToContainer.get -> System.Action<string!, object!>?
4749
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.MapErrorToContainer.set -> void
48-
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.MappingScopeName.get -> string!
4950
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.OnError.get -> System.Action<string!, System.FormattableString!, string?>?
5051
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.OnError.set -> void
5152
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.ParameterName.get -> string!
52-
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.RestrictToFormName.get -> string?
5353
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.Result.get -> object?
5454
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.SetResult(object? result) -> void
5555
Microsoft.AspNetCore.Components.Forms.Mapping.FormValueMappingContext.ValueType.get -> System.Type!

0 commit comments

Comments
 (0)