Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable

private int _nextComponentId;
private bool _isBatchInProgress;
private int _renderQueueDeferralDepth;
private ulong _lastEventHandlerId;
private List<Task>? _pendingTasks;
private Task? _disposeTask;
Expand Down Expand Up @@ -364,7 +365,7 @@ protected internal void RemoveRootComponent(int componentId)
_rootComponentsLatestParameters?.Remove(componentId);
}

ProcessRenderQueue();
ProcessRenderQueueIfNotDeferred();
}

/// <summary>
Expand Down Expand Up @@ -830,7 +831,32 @@ protected virtual void ProcessPendingRender()
return;
}

ProcessRenderQueue();
ProcessRenderQueueIfNotDeferred();
}

private void ProcessRenderQueueIfNotDeferred()
{
if (_renderQueueDeferralDepth == 0)
{
ProcessRenderQueue();
}
}

internal void BeginRenderQueueDeferral()
{
Dispatcher.AssertAccess();
_renderQueueDeferralDepth++;
}
Comment thread
PreethikaSelvam marked this conversation as resolved.

internal void EndRenderQueueDeferral(bool processPendingRender)
{
Dispatcher.AssertAccess();
Debug.Assert(_renderQueueDeferralDepth > 0);

if (--_renderQueueDeferralDepth == 0 && processPendingRender && !_isBatchInProgress)
{
ProcessPendingRender();
}
}

private void ProcessRenderQueue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,24 @@ public partial class StaticHtmlRenderer
/// <param name="output">The output destination.</param>
protected internal virtual void WriteComponentHtml(int componentId, TextWriter output)
{
// We're about to walk over some buffers inside the renderer that can be mutated during rendering.
// So, we require exclusive access to the renderer during this synchronous process.
// The frame buffers must remain stable for the whole synchronous walk. Dispatcher access prevents
// concurrent execution, and render queue deferral prevents reentrant rendering from mutating them.
// Updates queued during the walk are reflected the next time the component is written.
Dispatcher.AssertAccess();

var frames = GetCurrentRenderTreeFrames(componentId);
RenderFrames(componentId, output, frames, 0, frames.Count);
BeginRenderQueueDeferral();
var writeCompletedSuccessfully = false;
try
{
var frames = GetCurrentRenderTreeFrames(componentId);
RenderFrames(componentId, output, frames, 0, frames.Count);
writeCompletedSuccessfully = true;
}
finally
{
// If writing failed, leave queued work unprocessed so its failure cannot mask the writer exception.
EndRenderQueueDeferral(writeCompletedSuccessfully);
}
}

/// <summary>
Expand Down
161 changes: 161 additions & 0 deletions src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
using System.Web;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Forms.Mapping;
using Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Sections;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Microsoft.AspNetCore.Components.HtmlRendering;
Expand Down Expand Up @@ -831,6 +834,91 @@ await htmlRenderer.Dispatcher.InvokeAsync(async () =>
});
}

[Fact]
public async Task WriteHtmlTo_CanReplaceSectionContentWhileRenderingOutlet()
{
var services = GetServiceProvider();
await using var htmlRenderer = new SectionUpdatingStaticHtmlRenderer(services, NullLoggerFactory.Instance);

await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var outlet = htmlRenderer.BeginRenderingComponent(
new SectionOutlet(),
ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(SectionOutlet.SectionId), "testsection" }
}));
await outlet.QuiescenceTask;

var content = new UpdatingSectionContent();
var contentRoot = htmlRenderer.BeginRenderingComponent(content, ParameterView.Empty);
await contentRoot.QuiescenceTask;

htmlRenderer.BeforeRenderingSectionContent = content.Update;

Assert.Equal("initial", outlet.ToHtmlString());
Assert.Equal("updated", outlet.ToHtmlString());
});
}

[Fact]
public async Task WriteHtmlTo_CanRemoveSectionContentWhileRenderingOutlet()
{
var services = GetServiceProvider();
await using var htmlRenderer = new SectionUpdatingStaticHtmlRenderer(services, NullLoggerFactory.Instance);

await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var outlet = htmlRenderer.BeginRenderingComponent(
new SectionOutlet(),
ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(SectionOutlet.SectionId), "testsection" }
}));
await outlet.QuiescenceTask;

var content = new UpdatingSectionContent();
var contentRoot = htmlRenderer.BeginRenderingComponent(content, ParameterView.Empty);
await contentRoot.QuiescenceTask;

htmlRenderer.BeforeRenderingSectionContent = () => htmlRenderer.RemoveRootComponent(content);

Assert.Equal("initial", outlet.ToHtmlString());
Assert.Empty(outlet.ToHtmlString());
});
}

[Fact]
public async Task WriteHtmlTo_PreservesWritingExceptionAndRetainsDeferredRender()
{
var services = GetServiceProvider();
await using var htmlRenderer = new SectionUpdatingStaticHtmlRenderer(services, NullLoggerFactory.Instance);

await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var outlet = htmlRenderer.BeginRenderingComponent(
new SectionOutlet(),
ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(SectionOutlet.SectionId), "testsection" }
}));
await outlet.QuiescenceTask;

var content = new UpdatingSectionContent();
var contentRoot = htmlRenderer.BeginRenderingComponent(content, ParameterView.Empty);
await contentRoot.QuiescenceTask;

htmlRenderer.BeforeRenderingSectionContent = content.UpdateWithException;
using var writer = new ThrowingTextWriter();

var exception = Assert.Throws<IOException>(() => outlet.WriteHtmlTo(writer));
Assert.Equal("Writing failed.", exception.Message);

var renderException = Assert.Throws<InvalidOperationException>(() => outlet.ToHtmlString());
Assert.Equal("Rendering failed.", renderException.Message);
});
}

[Fact]
public async Task RenderComponentAsync_CanOutputToTextWriter()
{
Expand Down Expand Up @@ -1311,6 +1399,79 @@ public Task SetParametersAsync(ParameterView parameters)
}
}

private sealed class SectionUpdatingStaticHtmlRenderer(IServiceProvider services, ILoggerFactory loggerFactory)
: StaticHtmlRenderer(services, loggerFactory)
{
public Action BeforeRenderingSectionContent { get; set; }

public void RemoveRootComponent(IComponent component)
=> RemoveRootComponent(GetComponentState(component).ComponentId);

protected override void RenderChildComponent(TextWriter output, ref RenderTreeFrame componentFrame)
{
if (componentFrame.Component is SectionOutlet.SectionOutletContentRenderer)
{
var callback = BeforeRenderingSectionContent;
BeforeRenderingSectionContent = null;
callback?.Invoke();
}

base.RenderChildComponent(output, ref componentFrame);
}
}
Comment thread
PreethikaSelvam marked this conversation as resolved.

private sealed class UpdatingSectionContent : IComponent
{
private RenderHandle _renderHandle;
private RenderFragment _content = builder => builder.AddContent(0, "initial");

public void Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}

public Task SetParametersAsync(ParameterView parameters)
{
Render();
return Task.CompletedTask;
}

public void Update()
{
_content = builder => builder.AddContent(0, "updated");
Render();
}

public void UpdateWithException()
{
_content = _ => throw new InvalidOperationException("Rendering failed.");
Render();
}

private void Render()
{
_renderHandle.Render(builder =>
{
builder.OpenComponent<SectionContent>(0);
builder.AddComponentParameter(1, nameof(SectionContent.SectionId), "testsection");
builder.AddComponentParameter(2, nameof(SectionContent.ChildContent), _content);
builder.CloseComponent();
});
}
}

private sealed class ThrowingTextWriter : StringWriter
{
public override void Write(char value)
=> throw new IOException("Writing failed.");

public override void Write(string value)
=> throw new IOException("Writing failed.");

public override void Write(ReadOnlySpan<char> buffer)
=> throw new IOException("Writing failed.");
}

private class AsyncLoadingComponent : ComponentBase
{
string status;
Expand Down
Loading