Skip to content

Commit

Permalink
Dispatch forms with streaming rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
javiercn committed Apr 14, 2023
1 parent 65004fd commit 54fc180
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 22 deletions.
27 changes: 19 additions & 8 deletions src/Components/Components/src/Binding/CascadingModelBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,48 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection.Metadata;
using Microsoft.AspNetCore.Components.Binding;

namespace Microsoft.AspNetCore.Components.Binding;
namespace Microsoft.AspNetCore.Components;

internal class CascadingModelBinder : IComponent
/// <summary>
/// Defines the binding context for data bound from external sources.
/// </summary>
public class CascadingModelBinder : IComponent
{
private RenderHandle _handle;
private bool _hasRendered;
private ModelBindingContext? _bindingContext;

/// <summary>
/// The binding context name.
/// </summary>
[Parameter] public string Name { get; set; } = default!;

/// <summary>
/// Specifies the content to be rendered inside this <see cref="CascadingModelBinder"/>.
/// </summary>
[Parameter] public RenderFragment ChildContent { get; set; } = default!;

public void Attach(RenderHandle renderHandle)
void IComponent.Attach(RenderHandle renderHandle)
{
_handle = renderHandle;
}

public Task SetParametersAsync(ParameterView parameters)
Task IComponent.SetParametersAsync(ParameterView parameters)
{
if (!_hasRendered)
{
_hasRendered = true;
_bindingContext = new ModelBindingContext(Name);
parameters.SetParameterProperties(this);

_bindingContext = new ModelBindingContext(Name);
_handle.Render(builder =>
{
builder.OpenComponent<CascadingValue<ModelBindingContext>>(0);
builder.AddComponentParameter(1, "IsFixed", true);
builder.AddComponentParameter(2, "Value", _bindingContext);
builder.AddComponentParameter(3, "ChildContent", ChildContent);
builder.AddComponentParameter(1, nameof(CascadingValue<ModelBindingContext>.IsFixed), true);
builder.AddComponentParameter(2, nameof(CascadingValue<ModelBindingContext>.Value), _bindingContext);
builder.AddComponentParameter(3, nameof(CascadingValue<ModelBindingContext>.ChildContent), ChildContent);
builder.CloseComponent();
});
}
Expand Down
23 changes: 20 additions & 3 deletions src/Components/Components/src/Binding/ModelBindingContext.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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;

public class ModelBindingContext(string name)
/// <summary>
/// The binding context associated with a given model binding operation.
/// </summary>
public class ModelBindingContext
{
public string Name { get; } = name;
/// <summary>
/// Initializes a new instance of <see cref="ModelBindingContext"/>.
/// </summary>
/// <param name="name">The context name.</param>
public ModelBindingContext(string name)
{
ArgumentNullException.ThrowIfNull(name);

Name = name;
}

/// <summary>
/// The context name.
/// </summary>
public string Name { get; }
}
10 changes: 10 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
#nullable enable
Microsoft.AspNetCore.Components.Binding.ModelBindingContext
Microsoft.AspNetCore.Components.Binding.ModelBindingContext.ModelBindingContext(string! name) -> void
Microsoft.AspNetCore.Components.Binding.ModelBindingContext.Name.get -> string!
Microsoft.AspNetCore.Components.CascadingModelBinder
Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void
Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment!
Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.set -> void
Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string!
Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
Expand Down Expand Up @@ -41,5 +50,6 @@ override Microsoft.AspNetCore.Components.EventCallback<TValue>.GetHashCode() ->
override Microsoft.AspNetCore.Components.EventCallback<TValue>.Equals(object? obj) -> bool
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
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!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ShouldTrackNamedEventHandlers() -> bool
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.TrackNamedEventId(ulong eventHandlerId, int componentId, string! eventHandlerName) -> void
32 changes: 32 additions & 0 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,22 @@ protected virtual ComponentState CreateComponentState(int componentId, IComponen
/// has completed.
/// </returns>
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs)
{
return DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs, quiesce: false);
}

/// <summary>
/// Notifies the renderer that an event has occurred.
/// </summary>
/// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
/// <param name="fieldInfo">Information that the renderer can use to update the state of the existing render tree to match the UI.</param>
/// <param name="quiesce">Whether to wait for quiescence or not.</param>
/// <returns>
/// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
/// has completed.
/// </returns>
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs, bool quiesce)
{
Dispatcher.AssertAccess();

Expand Down Expand Up @@ -402,6 +418,17 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
_isBatchInProgress = true;

task = callback.InvokeAsync(eventArgs);
if (quiesce)
{
if (_ongoingQuiescenceTask == null)
{
_ongoingQuiescenceTask = task;
}
else
{
AddToPendingTasksWithErrorHandling(task, receiverComponentState);
}
}
}
catch (Exception e)
{
Expand All @@ -417,6 +444,11 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
ProcessPendingRender();
}

if (quiesce)
{
return WaitForQuiescence();
}

// Task completed synchronously or is still running. We already processed all of the rendering
// work that was queued so let our error handler deal with it.
var result = GetErrorHandledTask(task, receiverComponentState);
Expand Down
2 changes: 0 additions & 2 deletions src/Components/Components/src/RouteView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@

#nullable disable warnings

using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.AspNetCore.Components.Binding;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;

Expand Down
2 changes: 1 addition & 1 deletion src/Components/Endpoints/src/RazorComponentEndpointHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private void BuildRenderTree(RenderTreeBuilder builder)

builder.OpenComponent<LayoutView>(0);
builder.AddComponentParameter(1, nameof(LayoutView.Layout), pageLayoutType);
builder.AddComponentParameter(2, nameof(LayoutView.ChildContent), (RenderFragment)RenderPageWithParameters);
builder.AddComponentParameter(2, nameof(LayoutView.ChildContent), RenderPageWithParameters);
builder.CloseComponent();
}

Expand Down
16 changes: 10 additions & 6 deletions src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Buffers;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -32,7 +33,7 @@ private async Task RenderComponentCore()
{
_context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType;

if (await ValidateRequestAsync())
if (!await TryValidateRequestAsync(out var isPost))
{
// If the request is not valid we've already set the response to a 400 or similar
// and we can just exit early.
Expand All @@ -57,17 +58,19 @@ private async Task RenderComponentCore()
_context,
typeof(RazorComponentEndpointHost),
hostParameters,
waitForQuiescence: false);
waitForQuiescence: isPost);

var quiesceTask = isPost ? _renderer.DispatchCapturedEvent() : htmlContent.QuiescenceTask;

// Importantly, we must not yield this thread (which holds exclusive access to the renderer sync context)
// in between the first call to htmlContent.WriteTo and the point where we start listening for subsequent
// streaming SSR batches (inside SendStreamingUpdatesAsync). Otherwise some other code might dispatch to the
// renderer sync context and cause a batch that would get missed.
htmlContent.WriteTo(writer, HtmlEncoder.Default); // Don't use WriteToAsync, as per the comment above

if (!htmlContent.QuiescenceTask.IsCompleted)
if (!quiesceTask.IsCompleted)
{
await _renderer.SendStreamingUpdatesAsync(_context, htmlContent.QuiescenceTask, writer);
await _renderer.SendStreamingUpdatesAsync(_context, quiesceTask, writer);
}

// Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying
Expand All @@ -76,9 +79,10 @@ private async Task RenderComponentCore()
await writer.FlushAsync();
}

private Task<bool> ValidateRequestAsync()
private Task<bool> TryValidateRequestAsync(out bool isPost)
{
if (HttpMethods.IsPost(_context.Request.Method))
isPost = HttpMethods.IsPost(_context.Request.Method);
if (isPost)
{
return Task.FromResult(TrySetFormHandler());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ protected override void TrackNamedEventId(ulong eventHandlerId, int componentId,
}
}

internal Task DispatchCapturedEvent()
{
// Clear the list of non-streaming rendering tasks, since we've waited for quiesce before dispatching the event.
_nonStreamingPendingTasks.Clear();
return DispatchEventAsync(_capturedNamedEvent.EventHandlerId, null, EventArgs.Empty, quiesce: true);
}

private static string GenerateComponentPath(ComponentState state)
{
// We are generating a path from the root component with te component type names like:
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webview.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/Components/Web/src/Forms/EditForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Microsoft.AspNetCore.Components.Binding;
using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.Forms;
Expand Down Expand Up @@ -77,6 +78,11 @@ public EditContext? EditContext
/// </summary>
[Parameter] public EventCallback<EditContext> OnInvalidSubmit { get; set; }

/// <summary>
/// Gets the context associated with data bound to the EditContext in this form.
/// </summary>
[CascadingParameter] public ModelBindingContext? BindingContext { get; set; }

/// <inheritdoc />
protected override void OnParametersSet()
{
Expand Down Expand Up @@ -122,6 +128,10 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
builder.OpenElement(0, "form");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "onsubmit", _handleSubmitDelegate);
if (BindingContext != null)
{
builder.SetEventHandlerName(BindingContext.Name);
}
builder.OpenComponent<CascadingValue<EditContext>>(3);
builder.AddComponentParameter(4, "IsFixed", true);
builder.AddComponentParameter(5, "Value", _editContext);
Expand Down
2 changes: 2 additions & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#nullable enable
*REMOVED*override Microsoft.AspNetCore.Components.Forms.InputFile.OnInitialized() -> void
Microsoft.AspNetCore.Components.Forms.EditForm.BindingContext.get -> Microsoft.AspNetCore.Components.Binding.ModelBindingContext!
Microsoft.AspNetCore.Components.Forms.EditForm.BindingContext.set -> void
Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer
Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(System.Type! componentType, Microsoft.AspNetCore.Components.ParameterView initialParameters) -> Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent
Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.StaticHtmlRenderer(System.IServiceProvider! serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void
Expand Down

0 comments on commit 54fc180

Please sign in to comment.