From 128771d2cd2168e1b698ff65db3f9b87fb2f1f91 Mon Sep 17 00:00:00 2001 From: jspuij Date: Tue, 17 Mar 2020 16:42:22 +0100 Subject: [PATCH 1/3] Native compilation problems. --- src/WebWindow.Native/WebWindow.Native.vcxproj | 3 ++- src/WebWindow.Native/WebWindow.Windows.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/WebWindow.Native/WebWindow.Native.vcxproj b/src/WebWindow.Native/WebWindow.Native.vcxproj index e8d4df2..b28f3d8 100644 --- a/src/WebWindow.Native/WebWindow.Native.vcxproj +++ b/src/WebWindow.Native/WebWindow.Native.vcxproj @@ -29,7 +29,7 @@ - Application + DynamicLibrary true v142 Unicode @@ -95,6 +95,7 @@ true Windows + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Shlwapi.lib;%(AdditionalDependencies) diff --git a/src/WebWindow.Native/WebWindow.Windows.cpp b/src/WebWindow.Native/WebWindow.Windows.cpp index 5a5393f..c0b4140 100644 --- a/src/WebWindow.Native/WebWindow.Windows.cpp +++ b/src/WebWindow.Native/WebWindow.Windows.cpp @@ -371,7 +371,7 @@ void WebWindow::GetAllMonitors(GetAllMonitorsCallback callback) { if (callback) { - EnumDisplayMonitors(NULL, NULL, MonitorEnum, (LPARAM)callback); + EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)MonitorEnum, (LPARAM)callback); } } From 2cb53da929d2c945d641fc80ba789ac0b255b331 Mon Sep 17 00:00:00 2001 From: jspuij Date: Tue, 17 Mar 2020 16:43:51 +0100 Subject: [PATCH 2/3] Fix for https://github.com/SteveSandersonMS/WebWindow/issues/88 --- .../WebWindow.Blazor.JS.csproj | 4 + src/WebWindow.Blazor.JS/src/Boot.Desktop.ts | 10 +- src/WebWindow.Blazor.JS/src/RenderQueue.ts | 66 ++++++++ src/WebWindow.Blazor/DesktopRenderer.cs | 147 +++++++++++++++++- src/WebWindow.Blazor/JSInteropMethods.cs | 7 + 5 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 src/WebWindow.Blazor.JS/src/RenderQueue.ts 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/DesktopRenderer.cs b/src/WebWindow.Blazor/DesktopRenderer.cs index de6ddf8..7755c76 100644 --- a/src/WebWindow.Blazor/DesktopRenderer.cs +++ b/src/WebWindow.Blazor/DesktopRenderer.cs @@ -5,8 +5,10 @@ using Microsoft.Extensions.Logging; using Microsoft.JSInterop; using System; +using System.Collections.Concurrent; using System.IO; using System.Reflection; +using System.Threading; using System.Threading.Tasks; namespace WebWindows.Blazor @@ -17,11 +19,16 @@ namespace WebWindows.Blazor internal class DesktopRenderer : Renderer { + private static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true)); + private const int RendererId = 0; // Not relevant, since we have only one renderer in Desktop private readonly IPC _ipc; private readonly IJSRuntime _jsRuntime; private static readonly Type _writer; private static readonly MethodInfo _writeMethod; + internal readonly ConcurrentQueue _unacknowledgedRenderBatches = new ConcurrentQueue(); + private bool _disposing = false; + private long _nextRenderId = 1; public override Dispatcher Dispatcher { get; } = NullDispatcher.Instance; @@ -78,6 +85,12 @@ public Task AddComponentAsync(Type componentType, string domElementSelector) /// protected override Task UpdateDisplayAsync(in RenderBatch batch) { + if (_disposing) + { + // We are being disposed, so do no work. + return CanceledTask; + } + string base64; using (var memoryStream = new MemoryStream()) { @@ -91,13 +104,111 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch) base64 = Convert.ToBase64String(batchBytes); } - _ipc.Send("JS.RenderBatch", RendererId, base64); + var renderId = Interlocked.Increment(ref _nextRenderId); + + var pendingRender = new UnacknowledgedRenderBatch( + renderId, + new TaskCompletionSource()); + + // Buffer the rendered batches no matter what. We'll send it down immediately when the client + // is connected or right after the client reconnects. + + _unacknowledgedRenderBatches.Enqueue(pendingRender); - // TODO: Consider finding a way to get back a completion message from the Desktop side - // in case there was an error. We don't really need to wait for anything to happen, since - // this is not prerendering and we don't care how quickly the UI is updated, but it would - // be desirable to flow back errors. - return Task.CompletedTask; + _ipc.Send("JS.RenderBatch", renderId, base64); + + return pendingRender.CompletionSource.Task; + } + + public Task OnRenderCompletedAsync(long incomingBatchId, string errorMessageOrNull) + { + if (_disposing) + { + // Disposing so don't do work. + return Task.CompletedTask; + } + + // When clients send acks we know for sure they received and applied the batch. + // We send batches right away, and hold them in memory until we receive an ACK. + // If one or more client ACKs get lost (e.g., with long polling, client->server delivery is not guaranteed) + // we might receive an ack for a higher batch. + // We confirm all previous batches at that point (because receiving an ack is guarantee + // from the client that it has received and successfully applied all batches up to that point). + + // If receive an ack for a previously acknowledged batch, its an error, as the messages are + // guaranteed to be delivered in order, so a message for a render batch of 2 will never arrive + // after a message for a render batch for 3. + // If that were to be the case, it would just be enough to relax the checks here and simply skip + // the message. + + // A batch might get lost when we send it to the client, because the client might disconnect before receiving and processing it. + // In this case, once it reconnects the server will re-send any unacknowledged batches, some of which the + // client might have received and even believe it did send back an acknowledgement for. The client handles + // those by re-acknowledging. + + // Even though we're not on the renderer sync context here, it's safe to assume ordered execution of the following + // line (i.e., matching the order in which we received batch completion messages) based on the fact that SignalR + // synchronizes calls to hub methods. That is, it won't issue more than one call to this method from the same hub + // at the same time on different threads. + + if (!_unacknowledgedRenderBatches.TryPeek(out var nextUnacknowledgedBatch) || incomingBatchId < nextUnacknowledgedBatch.BatchId) + { + // TODO: Log duplicated batch ack. + return Task.CompletedTask; + } + else + { + var lastBatchId = nextUnacknowledgedBatch.BatchId; + // Order is important here so that we don't prematurely dequeue the last nextUnacknowledgedBatch + while (_unacknowledgedRenderBatches.TryPeek(out nextUnacknowledgedBatch) && nextUnacknowledgedBatch.BatchId <= incomingBatchId) + { + lastBatchId = nextUnacknowledgedBatch.BatchId; + // At this point the queue is definitely not full, we have at least emptied one slot, so we allow a further + // full queue log entry the next time it fills up. + _unacknowledgedRenderBatches.TryDequeue(out _); + ProcessPendingBatch(errorMessageOrNull, nextUnacknowledgedBatch); + } + + if (lastBatchId < incomingBatchId) + { + // This exception is due to a bad client input, so we mark it as such to prevent logging it as a warning and + // flooding the logs with warnings. + throw new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'."); + } + + // Normally we will not have pending renders, but it might happen that we reached the limit of + // available buffered renders and new renders got queued. + // Invoke ProcessBufferedRenderRequests so that we might produce any additional batch that is + // missing. + + // We return the task in here, but the caller doesn't await it. + return Dispatcher.InvokeAsync(() => + { + // Now we're on the sync context, check again whether we got disposed since this + // work item was queued. If so there's nothing to do. + if (!_disposing) + { + ProcessPendingRender(); + } + }); + } + } + + private void ProcessPendingBatch(string errorMessageOrNull, UnacknowledgedRenderBatch entry) + { + CompleteRender(entry.CompletionSource, errorMessageOrNull); + } + + private void CompleteRender(TaskCompletionSource pendingRenderInfo, string errorMessageOrNull) + { + if (errorMessageOrNull == null) + { + pendingRenderInfo.TrySetResult(null); + } + else + { + pendingRenderInfo.TrySetException(new InvalidOperationException(errorMessageOrNull)); + } } private async void CaptureAsyncExceptions(ValueTask task) @@ -116,5 +227,29 @@ protected override void HandleException(Exception exception) { Console.WriteLine(exception.ToString()); } + + /// + protected override void Dispose(bool disposing) + { + _disposing = true; + while (_unacknowledgedRenderBatches.TryDequeue(out var entry)) + { + entry.CompletionSource.TrySetCanceled(); + } + base.Dispose(true); + } + + internal readonly struct UnacknowledgedRenderBatch + { + public UnacknowledgedRenderBatch(long batchId, TaskCompletionSource completionSource) + { + BatchId = batchId; + CompletionSource = completionSource; + } + + public long BatchId { get; } + + public TaskCompletionSource CompletionSource { get; } + } } } diff --git a/src/WebWindow.Blazor/JSInteropMethods.cs b/src/WebWindow.Blazor/JSInteropMethods.cs index e6aba40..cb3fb06 100644 --- a/src/WebWindow.Blazor/JSInteropMethods.cs +++ b/src/WebWindow.Blazor/JSInteropMethods.cs @@ -26,5 +26,12 @@ public static void NotifyLocationChanged(string uri, bool isInterceptedLink) { DesktopNavigationManager.Instance.SetLocation(uri, isInterceptedLink); } + + [JSInvokable(nameof(OnRenderCompleted))] + public static async Task OnRenderCompleted(long renderId, string errorMessageOrNull) + { + var renderer = ComponentsDesktop.DesktopRenderer; + await renderer.OnRenderCompletedAsync(renderId, errorMessageOrNull); + } } } From ecab5de91d30cdef739ef7984b1a2badae06a240 Mon Sep 17 00:00:00 2001 From: jspuij Date: Fri, 20 Mar 2020 12:07:12 +0100 Subject: [PATCH 3/3] Added a proper dispatcher and fixed a few threading issues. --- src/WebWindow.Blazor/ComponentsDesktop.cs | 29 +-- src/WebWindow.Blazor/DesktopDispatcher.cs | 203 ++++++++++++++++++ src/WebWindow.Blazor/DesktopRenderer.cs | 7 +- .../DesktopSynchronizationContext.cs | 10 +- src/WebWindow.Blazor/NullDispatcher.cs | 58 ----- testassets/MyBlazorApp/Pages/WindowProp.razor | 4 +- 6 files changed, 230 insertions(+), 81 deletions(-) create mode 100644 src/WebWindow.Blazor/DesktopDispatcher.cs delete mode 100644 src/WebWindow.Blazor/NullDispatcher.cs 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(); + + this.context.Post( + state => + { + var taskCompletionSource = (TaskCompletionSource)state; + try + { + workItem(); + taskCompletionSource.SetResult(null); + } + catch (OperationCanceledException) + { + taskCompletionSource.SetCanceled(); + } + catch (Exception exception) + { + taskCompletionSource.SetException(exception); + } + }, taskCompletionSource); + + return taskCompletionSource.Task; + } + + /// + /// Invokes the given System.Func'1 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(Func workItem) + { + if (this.CheckAccess()) + { + return workItem(); + } + + var taskCompletionSource = new TaskCompletionSource(); + + this.context.Post( + async state => + { + var taskCompletionSource = (TaskCompletionSource)state; + try + { + await workItem(); + taskCompletionSource.SetResult(null); + } + catch (OperationCanceledException) + { + taskCompletionSource.SetCanceled(); + } + catch (Exception exception) + { + taskCompletionSource.SetException(exception); + } + }, taskCompletionSource); + + return taskCompletionSource.Task; + } + + /// + /// Invokes the given System.Func'1 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. + /// + /// The return type. + public override Task InvokeAsync(Func workItem) + { + if (this.CheckAccess()) + { + return Task.FromResult(workItem()); + } + + var taskCompletionSource = new TaskCompletionSource(); + + this.context.Post( + state => + { + var taskCompletionSource = (TaskCompletionSource)state; + try + { + TResult result = workItem(); + taskCompletionSource.SetResult(result); + } + catch (OperationCanceledException) + { + taskCompletionSource.SetCanceled(); + } + catch (Exception exception) + { + taskCompletionSource.SetException(exception); + } + }, taskCompletionSource); + + return taskCompletionSource.Task; + } + + /// + /// Invokes the given System.Func'1 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. + /// + /// The return type. + public override Task InvokeAsync(Func> workItem) + { + if (this.CheckAccess()) + { + return workItem(); + } + + var taskCompletionSource = new TaskCompletionSource(); + + this.context.Post( + async state => + { + var taskCompletionSource = (TaskCompletionSource)state; + try + { + TResult result = await workItem(); + taskCompletionSource.SetResult(result); + } + catch (OperationCanceledException) + { + taskCompletionSource.SetCanceled(); + } + catch (Exception exception) + { + taskCompletionSource.SetException(exception); + } + }, taskCompletionSource); + + return taskCompletionSource.Task; + } + } +} \ No newline at end of file diff --git a/src/WebWindow.Blazor/DesktopRenderer.cs b/src/WebWindow.Blazor/DesktopRenderer.cs index 7755c76..308fcb9 100644 --- a/src/WebWindow.Blazor/DesktopRenderer.cs +++ b/src/WebWindow.Blazor/DesktopRenderer.cs @@ -30,7 +30,7 @@ internal class DesktopRenderer : Renderer private bool _disposing = false; private long _nextRenderId = 1; - public override Dispatcher Dispatcher { get; } = NullDispatcher.Instance; + public override Dispatcher Dispatcher { get; } static DesktopRenderer() { @@ -38,11 +38,12 @@ static DesktopRenderer() _writeMethod = _writer.GetMethod("Write", new[] { typeof(RenderBatch).MakeByRefType() }); } - public DesktopRenderer(IServiceProvider serviceProvider, IPC ipc, ILoggerFactory loggerFactory) + public DesktopRenderer(IServiceProvider serviceProvider, IPC ipc, ILoggerFactory loggerFactory, IJSRuntime jSRuntime, Dispatcher dispatcher) : base(serviceProvider, loggerFactory) { _ipc = ipc ?? throw new ArgumentNullException(nameof(ipc)); - _jsRuntime = serviceProvider.GetRequiredService(); + Dispatcher = dispatcher; + _jsRuntime = jSRuntime; } /// diff --git a/src/WebWindow.Blazor/DesktopSynchronizationContext.cs b/src/WebWindow.Blazor/DesktopSynchronizationContext.cs index 18bb45d..ab0de1d 100644 --- a/src/WebWindow.Blazor/DesktopSynchronizationContext.cs +++ b/src/WebWindow.Blazor/DesktopSynchronizationContext.cs @@ -7,13 +7,13 @@ namespace WebWindows.Blazor { internal class DesktopSynchronizationContext : SynchronizationContext { - public static event EventHandler UnhandledException; + public event EventHandler UnhandledException; private readonly WorkQueue _work; public DesktopSynchronizationContext(CancellationToken cancellationToken) { - _work = new WorkQueue(cancellationToken); + _work = new WorkQueue(this, cancellationToken); } public override SynchronizationContext CreateCopy() @@ -59,10 +59,12 @@ public static void CheckAccess() private class WorkQueue { private readonly Thread _thread; + private readonly DesktopSynchronizationContext _desktopSynchronizationContext; private readonly CancellationToken _cancellationToken; - public WorkQueue(CancellationToken cancellationToken) + public WorkQueue(DesktopSynchronizationContext desktopSynchronizationContext, CancellationToken cancellationToken) { + _desktopSynchronizationContext = desktopSynchronizationContext; _cancellationToken = cancellationToken; _thread = new Thread(ProcessQueue); _thread.Start(); @@ -120,7 +122,7 @@ public void ProcessWorkitemInline(SendOrPostCallback callback, object state) } catch (Exception e) { - UnhandledException?.Invoke(this, e); + _desktopSynchronizationContext.UnhandledException?.Invoke(this, e); } } } diff --git a/src/WebWindow.Blazor/NullDispatcher.cs b/src/WebWindow.Blazor/NullDispatcher.cs deleted file mode 100644 index e22f6bb..0000000 --- a/src/WebWindow.Blazor/NullDispatcher.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; - -namespace WebWindows.Blazor -{ - internal class NullDispatcher : Dispatcher - { - public static readonly Dispatcher Instance = new NullDispatcher(); - - private NullDispatcher() - { - } - - public override bool CheckAccess() => true; - - public override Task InvokeAsync(Action workItem) - { - if (workItem is null) - { - throw new ArgumentNullException(nameof(workItem)); - } - - workItem(); - return Task.CompletedTask; - } - - public override Task InvokeAsync(Func workItem) - { - if (workItem is null) - { - throw new ArgumentNullException(nameof(workItem)); - } - - return workItem(); - } - - public override Task InvokeAsync(Func workItem) - { - if (workItem is null) - { - throw new ArgumentNullException(nameof(workItem)); - } - - return Task.FromResult(workItem()); - } - - public override Task InvokeAsync(Func> workItem) - { - if (workItem is null) - { - throw new ArgumentNullException(nameof(workItem)); - } - - return workItem(); - } - } -} \ No newline at end of file diff --git a/testassets/MyBlazorApp/Pages/WindowProp.razor b/testassets/MyBlazorApp/Pages/WindowProp.razor index 1ab6455..4d3af58 100644 --- a/testassets/MyBlazorApp/Pages/WindowProp.razor +++ b/testassets/MyBlazorApp/Pages/WindowProp.razor @@ -55,8 +55,8 @@ @code { protected override void OnInitialized() { - Window.SizeChanged += (sender, e) => StateHasChanged(); - Window.LocationChanged += (sender, e) => StateHasChanged(); + Window.SizeChanged += (sender, e) => InvokeAsync(StateHasChanged); + Window.LocationChanged += (sender, e) => InvokeAsync(StateHasChanged); } string iconFilename;