diff --git a/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj b/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj
index 02d5e16..9a64e07 100644
--- a/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj
+++ b/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj
@@ -15,6 +15,10 @@
+
+
+
+
diff --git a/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts b/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts
index fbe9875..889f1fe 100644
--- a/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts
+++ b/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts
@@ -1,17 +1,17 @@
import '@dotnet/jsinterop/dist/Microsoft.JSInterop';
import '@browserjs/GlobalExports';
-import { OutOfProcessRenderBatch } from '@browserjs/Rendering/RenderBatch/OutOfProcessRenderBatch';
import { setEventDispatcher } from '@browserjs/Rendering/RendererEventDispatcher';
import { internalFunctions as navigationManagerFunctions } from '@browserjs/Services/NavigationManager';
-import { renderBatch } from '@browserjs/Rendering/Renderer';
import { decode } from 'base64-arraybuffer';
import * as ipc from './IPC';
+import { RenderQueue } from './RenderQueue';
function boot() {
setEventDispatcher((eventDescriptor, eventArgs) => DotNet.invokeMethodAsync('WebWindow.Blazor', 'DispatchEvent', eventDescriptor, JSON.stringify(eventArgs)));
navigationManagerFunctions.listenForNavigationEvents((uri: string, intercepted: boolean) => {
return DotNet.invokeMethodAsync('WebWindow.Blazor', 'NotifyLocationChanged', uri, intercepted);
});
+ const renderQueue = RenderQueue.getOrCreate();
// Configure the mechanism for JS<->NET calls
DotNet.attachDispatcher({
@@ -33,9 +33,9 @@ function boot() {
DotNet.jsCallDispatcher.endInvokeDotNetFromJS(callId, success, resultOrError);
});
- ipc.on('JS.RenderBatch', (rendererId, batchBase64) => {
- var batchData = new Uint8Array(decode(batchBase64));
- renderBatch(rendererId, new OutOfProcessRenderBatch(batchData));
+ ipc.on('JS.RenderBatch', (batchId, batchBase64) => {
+ const batchData = new Uint8Array(decode(batchBase64));
+ renderQueue.processBatch(batchId, batchData);
});
ipc.on('JS.Error', (message) => {
diff --git a/src/WebWindow.Blazor.JS/src/RenderQueue.ts b/src/WebWindow.Blazor.JS/src/RenderQueue.ts
new file mode 100644
index 0000000..07e6b0d
--- /dev/null
+++ b/src/WebWindow.Blazor.JS/src/RenderQueue.ts
@@ -0,0 +1,66 @@
+import '@dotnet/jsinterop/dist/Microsoft.JSInterop';
+import { renderBatch } from '@browserjs/Rendering/Renderer';
+import { OutOfProcessRenderBatch } from '@browserjs/Rendering/RenderBatch/OutOfProcessRenderBatch';
+
+export class RenderQueue {
+ private static instance: RenderQueue;
+
+ private nextBatchId = 2;
+
+ private fatalError?: string;
+
+ public browserRendererId: number;
+
+ public constructor(browserRendererId: number) {
+ this.browserRendererId = browserRendererId;
+ }
+
+ public static getOrCreate(): RenderQueue {
+ if (!RenderQueue.instance) {
+ RenderQueue.instance = new RenderQueue(0);
+ }
+
+ return this.instance;
+ }
+
+ public async processBatch(receivedBatchId: number, batchData: Uint8Array): Promise {
+ if (receivedBatchId < this.nextBatchId) {
+ await this.completeBatch(receivedBatchId);
+ return;
+ }
+
+ if (receivedBatchId > this.nextBatchId) {
+ if (this.fatalError) {
+ console.log(`Received a new batch ${receivedBatchId} but errored out on a previous batch ${this.nextBatchId - 1}`);
+ await DotNet.invokeMethodAsync('WebWindow.Blazor', 'OnRenderCompleted', this.nextBatchId - 1, this.fatalError.toString());
+ return;
+ }
+ return;
+ }
+
+ try {
+ this.nextBatchId++;
+ renderBatch(this.browserRendererId, new OutOfProcessRenderBatch(batchData));
+ await this.completeBatch(receivedBatchId);
+ } catch (error) {
+ this.fatalError = error.toString();
+ console.error(`There was an error applying batch ${receivedBatchId}.`);
+
+ // If there's a rendering exception, notify server *and* throw on client
+ DotNet.invokeMethodAsync('WebWindow.Blazor', 'OnRenderCompleted', receivedBatchId, error.toString());
+ throw error;
+ }
+ }
+
+ public getLastBatchid(): number {
+ return this.nextBatchId - 1;
+ }
+
+ private async completeBatch(batchId: number): Promise {
+ try {
+ await DotNet.invokeMethodAsync('WebWindow.Blazor', 'OnRenderCompleted', batchId, null);
+ } catch {
+ console.warn(`Failed to deliver completion notification for render '${batchId}'.`);
+ }
+ }
+}
diff --git a/src/WebWindow.Blazor/ComponentsDesktop.cs b/src/WebWindow.Blazor/ComponentsDesktop.cs
index 08402c7..2163a04 100644
--- a/src/WebWindow.Blazor/ComponentsDesktop.cs
+++ b/src/WebWindow.Blazor/ComponentsDesktop.cs
@@ -25,10 +25,6 @@ public static class ComponentsDesktop
public static void Run(string windowTitle, string hostHtmlPath)
{
- DesktopSynchronizationContext.UnhandledException += (sender, exception) =>
- {
- UnhandledException(exception);
- };
WebWindow = new WebWindow(windowTitle, options =>
{
@@ -120,9 +116,17 @@ private static async Task RunAsync(IPC ipc, CancellationToken appLifet
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true);
+ var dispatcher = new PlatformDispatcher(appLifetime);
+
+ dispatcher.Context.UnhandledException += (sender, exception) =>
+ {
+ UnhandledException(exception);
+ };
+
DesktopJSRuntime = new DesktopJSRuntime(ipc);
await PerformHandshakeAsync(ipc);
- AttachJsInterop(ipc, appLifetime);
+
+ AttachJsInterop(ipc, dispatcher.Context, appLifetime);
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(configurationBuilder.Build());
@@ -141,7 +145,7 @@ private static async Task RunAsync(IPC ipc, CancellationToken appLifet
var loggerFactory = services.GetRequiredService();
- DesktopRenderer = new DesktopRenderer(services, ipc, loggerFactory);
+ DesktopRenderer = new DesktopRenderer(services, ipc, loggerFactory, DesktopJSRuntime, dispatcher);
DesktopRenderer.UnhandledException += (sender, exception) =>
{
Console.Error.WriteLine(exception);
@@ -149,7 +153,7 @@ private static async Task RunAsync(IPC ipc, CancellationToken appLifet
foreach (var rootComponent in builder.Entries)
{
- _ = DesktopRenderer.AddComponentAsync(rootComponent.componentType, rootComponent.domElementSelector);
+ await dispatcher.InvokeAsync(async () => await DesktopRenderer.AddComponentAsync(rootComponent.componentType, rootComponent.domElementSelector));
}
}
@@ -179,14 +183,11 @@ private static async Task PerformHandshakeAsync(IPC ipc)
await tcs.Task;
}
- private static void AttachJsInterop(IPC ipc, CancellationToken appLifetime)
- {
- var desktopSynchronizationContext = new DesktopSynchronizationContext(appLifetime);
- SynchronizationContext.SetSynchronizationContext(desktopSynchronizationContext);
-
+ private static void AttachJsInterop(IPC ipc, SynchronizationContext synchronizationContext, CancellationToken appLifetime)
+ {
ipc.On("BeginInvokeDotNetFromJS", args =>
{
- desktopSynchronizationContext.Send(state =>
+ synchronizationContext.Send(state =>
{
var argsArray = (object[])state;
DotNetDispatcher.BeginInvokeDotNet(
@@ -202,7 +203,7 @@ private static void AttachJsInterop(IPC ipc, CancellationToken appLifetime)
ipc.On("EndInvokeJSFromDotNet", args =>
{
- desktopSynchronizationContext.Send(state =>
+ synchronizationContext.Send(state =>
{
var argsArray = (object[])state;
DotNetDispatcher.EndInvokeJS(
diff --git a/src/WebWindow.Blazor/DesktopDispatcher.cs b/src/WebWindow.Blazor/DesktopDispatcher.cs
new file mode 100644
index 0000000..12a6dd6
--- /dev/null
+++ b/src/WebWindow.Blazor/DesktopDispatcher.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+
+namespace WebWindows.Blazor
+{
+ ///
+ /// A dispatcher that does not dispatch but invokes directly.
+ ///
+ internal class PlatformDispatcher : Dispatcher
+ {
+ private readonly DesktopSynchronizationContext context;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The cancellation token to pass to the synchronizationcontext.
+ public PlatformDispatcher(CancellationToken cancellationToken)
+ {
+ this.context = new DesktopSynchronizationContext(cancellationToken);
+ this.context.UnhandledException += (sender, e) =>
+ {
+ this.OnUnhandledException(new UnhandledExceptionEventArgs(e, false));
+ };
+ }
+
+ ///
+ /// Gets and internal reference to the context.
+ ///
+ internal DesktopSynchronizationContext Context => this.context;
+
+ ///
+ /// Returns a value that determines whether using the dispatcher to invoke a work
+ /// item is required from the current context.
+ ///
+ /// true if invoking is required, otherwise false.
+ public override bool CheckAccess() => System.Threading.SynchronizationContext.Current == this.context;
+
+ ///
+ /// Invokes the given System.Action in the context of the associated
+ /// Microsoft.AspNetCore.Components.RenderTree.Renderer.
+ ///
+ /// The action to execute.
+ ///
+ /// A System.Threading.Tasks.Task that will be completed when the action has finished
+ /// executing.
+ ///
+ public override Task InvokeAsync(Action workItem)
+ {
+ if (this.CheckAccess())
+ {
+ workItem();
+ return Task.CompletedTask;
+ }
+
+ var taskCompletionSource = new TaskCompletionSource