Skip to content

Commit a00137a

Browse files
mattleibowCopilot
andcommitted
Harden AppType startup: await render quiescence, dispose renderer, scope MapAppType
Addresses correctness concerns from PR review feedback: - HybridHostPageRenderer.Render now awaits the host component's QuiescenceTask before serializing to HTML, so asynchronously-initialized host content (e.g. OnInitializedAsync) and its render-mode registrations are not dropped from the generated document. Mirrors the framework's own static HTML rendering. - The static renderer is now disposed (on its dispatcher) after each host render instead of being leaked. - MapAppType early-returns when AppType is null, leaving the legacy HostPage startup path completely untouched. MacCatalyst device tests: 45 passed, 1 pre-existing skip (AppType host/mount, dynamic head, @assets fingerprinting, and legacy paths all green). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2ca68fe1-bef6-46a6-a0fd-17b7654cef34
1 parent 1151747 commit a00137a

2 files changed

Lines changed: 28 additions & 6 deletions

File tree

src/BlazorWebView/src/Maui/BlazorWebViewHandler.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ public static void MapHostPage(BlazorWebViewHandler handler, IBlazorWebView webV
8282
public static void MapAppType(BlazorWebViewHandler handler, IBlazorWebView webView)
8383
{
8484
#if !(NETSTANDARD || !PLATFORM)
85+
// Only views that opt into AppType need this mapper. When AppType is null the legacy
86+
// HostPage startup path is left completely untouched (MapHostPage already handled it).
87+
if (webView.AppType is null)
88+
{
89+
return;
90+
}
91+
8592
// AppType provides a synthetic HostPage, so ensure the handler picks it up and attempts startup.
8693
handler.HostPage = webView.HostPage;
8794
handler.StartWebViewCoreIfPossible();

src/BlazorWebView/src/Maui/HybridHostPageRenderer.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,31 @@ public static HybridHostPageResult Render(
7575
var resolvedAssets = assets ?? ResourceAssetCollection.Empty;
7676

7777
// Render on a thread-pool thread so the renderer's dispatcher never contends with the UI
78-
// synchronization context. The render is synchronous (interactive components are replaced
79-
// with static placeholders), so this completes without real blocking.
78+
// synchronization context.
8079
return Task.Run(() =>
8180
{
8281
var renderer = new HybridHostPageRenderer(services, loggerFactory, resolvedAssets);
83-
return renderer.Dispatcher.InvokeAsync(() =>
82+
return renderer.Dispatcher.InvokeAsync(async () =>
8483
{
85-
var rootComponent = renderer.BeginRenderingComponent(appComponentType, ParameterView.Empty);
86-
var html = rootComponent.ToHtmlString();
87-
return new HybridHostPageResult(html, renderer._registrations);
84+
try
85+
{
86+
var rootComponent = renderer.BeginRenderingComponent(appComponentType, ParameterView.Empty);
87+
88+
// Wait for the component tree to finish rendering - including any asynchronous
89+
// initialization (for example OnInitializedAsync) - before serializing, otherwise
90+
// async host content (and its render-mode registrations) could be omitted from the
91+
// document. This mirrors how the framework's own static HTML rendering awaits quiescence.
92+
await rootComponent.QuiescenceTask.ConfigureAwait(false);
93+
94+
var html = rootComponent.ToHtmlString();
95+
return new HybridHostPageResult(html, renderer._registrations);
96+
}
97+
finally
98+
{
99+
// Dispose on the renderer's dispatcher (required by Renderer) to release the
100+
// component tree and avoid leaking the renderer per host render.
101+
renderer.Dispose();
102+
}
88103
});
89104
}).GetAwaiter().GetResult();
90105
}

0 commit comments

Comments
 (0)