Skip to content
This repository was archived by the owner on Jun 5, 2023. It is now read-only.
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
4 changes: 4 additions & 0 deletions src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<WebpackInputs Include="**\*.ts" Exclude="node_modules\**" />
</ItemGroup>

<ItemGroup>
<WebpackInputs Remove="src\RenderQueue.ts" />
</ItemGroup>

<ItemGroup>
<Folder Include="dist\" />
</ItemGroup>
Expand Down
10 changes: 5 additions & 5 deletions src/WebWindow.Blazor.JS/src/Boot.Desktop.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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) => {
Expand Down
66 changes: 66 additions & 0 deletions src/WebWindow.Blazor.JS/src/RenderQueue.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
try {
await DotNet.invokeMethodAsync('WebWindow.Blazor', 'OnRenderCompleted', batchId, null);
} catch {
console.warn(`Failed to deliver completion notification for render '${batchId}'.`);
}
}
}
29 changes: 15 additions & 14 deletions src/WebWindow.Blazor/ComponentsDesktop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ public static class ComponentsDesktop

public static void Run<TStartup>(string windowTitle, string hostHtmlPath)
{
DesktopSynchronizationContext.UnhandledException += (sender, exception) =>
{
UnhandledException(exception);
};

WebWindow = new WebWindow(windowTitle, options =>
{
Expand Down Expand Up @@ -120,9 +116,17 @@ private static async Task RunAsync<TStartup>(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<IConfiguration>(configurationBuilder.Build());
Expand All @@ -141,15 +145,15 @@ private static async Task RunAsync<TStartup>(IPC ipc, CancellationToken appLifet

var loggerFactory = services.GetRequiredService<ILoggerFactory>();

DesktopRenderer = new DesktopRenderer(services, ipc, loggerFactory);
DesktopRenderer = new DesktopRenderer(services, ipc, loggerFactory, DesktopJSRuntime, dispatcher);
DesktopRenderer.UnhandledException += (sender, exception) =>
{
Console.Error.WriteLine(exception);
};

foreach (var rootComponent in builder.Entries)
{
_ = DesktopRenderer.AddComponentAsync(rootComponent.componentType, rootComponent.domElementSelector);
await dispatcher.InvokeAsync(async () => await DesktopRenderer.AddComponentAsync(rootComponent.componentType, rootComponent.domElementSelector));
}
}

Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
203 changes: 203 additions & 0 deletions src/WebWindow.Blazor/DesktopDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

namespace WebWindows.Blazor
{
/// <summary>
/// A dispatcher that does not dispatch but invokes directly.
/// </summary>
internal class PlatformDispatcher : Dispatcher
{
private readonly DesktopSynchronizationContext context;

/// <summary>
/// Initializes a new instance of the <see cref="PlatformDispatcher"/> class.
/// </summary>
/// <param name="cancellationToken">The cancellation token to pass to the synchronizationcontext.</param>
public PlatformDispatcher(CancellationToken cancellationToken)
{
this.context = new DesktopSynchronizationContext(cancellationToken);
this.context.UnhandledException += (sender, e) =>
{
this.OnUnhandledException(new UnhandledExceptionEventArgs(e, false));
};
}

/// <summary>
/// Gets and internal reference to the context.
/// </summary>
internal DesktopSynchronizationContext Context => this.context;

/// <summary>
/// Returns a value that determines whether using the dispatcher to invoke a work
/// item is required from the current context.
/// </summary>
/// <returns> true if invoking is required, otherwise false.</returns>
public override bool CheckAccess() => System.Threading.SynchronizationContext.Current == this.context;

/// <summary>
/// Invokes the given System.Action in the context of the associated
/// Microsoft.AspNetCore.Components.RenderTree.Renderer.
/// </summary>
/// <param name="workItem">The action to execute.</param>
/// <returns>
/// A System.Threading.Tasks.Task that will be completed when the action has finished
/// executing.
/// </returns>
public override Task InvokeAsync(Action workItem)
{
if (this.CheckAccess())
{
workItem();
return Task.CompletedTask;
}

var taskCompletionSource = new TaskCompletionSource<object>();

this.context.Post(
state =>
{
var taskCompletionSource = (TaskCompletionSource<object>)state;
try
{
workItem();
taskCompletionSource.SetResult(null);
}
catch (OperationCanceledException)
{
taskCompletionSource.SetCanceled();
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
}, taskCompletionSource);

return taskCompletionSource.Task;
}

/// <summary>
/// Invokes the given System.Func'1 in the context of the associated
/// Microsoft.AspNetCore.Components.RenderTree.Renderer.
/// </summary>
/// <param name="workItem">The action to execute.</param>
/// <returns>
/// A System.Threading.Tasks.Task that will be completed when the action has finished
/// executing.
/// </returns>
public override Task InvokeAsync(Func<Task> workItem)
{
if (this.CheckAccess())
{
return workItem();
}

var taskCompletionSource = new TaskCompletionSource<object>();

this.context.Post(
async state =>
{
var taskCompletionSource = (TaskCompletionSource<object>)state;
try
{
await workItem();
taskCompletionSource.SetResult(null);
}
catch (OperationCanceledException)
{
taskCompletionSource.SetCanceled();
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
}, taskCompletionSource);

return taskCompletionSource.Task;
}

/// <summary>
/// Invokes the given System.Func'1 in the context of the associated
/// Microsoft.AspNetCore.Components.RenderTree.Renderer.
/// </summary>
/// <param name="workItem">The action to execute.</param>
/// <returns>
/// A System.Threading.Tasks.Task that will be completed when the action has finished
/// executing.
/// </returns>
/// <typeparam name="TResult">The return type.</typeparam>
public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
{
if (this.CheckAccess())
{
return Task.FromResult(workItem());
}

var taskCompletionSource = new TaskCompletionSource<TResult>();

this.context.Post(
state =>
{
var taskCompletionSource = (TaskCompletionSource<TResult>)state;
try
{
TResult result = workItem();
taskCompletionSource.SetResult(result);
}
catch (OperationCanceledException)
{
taskCompletionSource.SetCanceled();
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
}, taskCompletionSource);

return taskCompletionSource.Task;
}

/// <summary>
/// Invokes the given System.Func'1 in the context of the associated
/// Microsoft.AspNetCore.Components.RenderTree.Renderer.
/// </summary>
/// <param name="workItem">The action to execute.</param>
/// <returns>
/// A System.Threading.Tasks.Task that will be completed when the action has finished
/// executing.
/// </returns>
/// <typeparam name="TResult">The return type.</typeparam>
public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
{
if (this.CheckAccess())
{
return workItem();
}

var taskCompletionSource = new TaskCompletionSource<TResult>();

this.context.Post(
async state =>
{
var taskCompletionSource = (TaskCompletionSource<TResult>)state;
try
{
TResult result = await workItem();
taskCompletionSource.SetResult(result);
}
catch (OperationCanceledException)
{
taskCompletionSource.SetCanceled();
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
}, taskCompletionSource);

return taskCompletionSource.Task;
}
}
}
Loading