From e5b2856a5b489ade137cd3fa8f0b493f370383f5 Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Wed, 3 Apr 2024 08:05:06 +0900 Subject: [PATCH] Improve JavaScript interop pattern --- BlazingStory/Components/BlazingStoryApp.razor | 10 ++--- .../Components/BlazingStoryApp.razor.js | 17 +------- .../Components/BlazingStoryApp.razor.ts | 23 ++--------- .../Components/Menus/PopupMenu.razor | 4 +- .../Components/Preview/PreviewFrame.razor | 3 +- .../Internals/Extensions/IJSExtensions.cs | 40 ++++++++++++++++-- .../Internals/Pages/Canvas/MeasureLayer.razor | 20 ++++----- .../Pages/Canvas/MeasureLayer.razor.js | 34 +++++++-------- .../Pages/Canvas/MeasureLayer.razor.ts | 41 +++++++++---------- BlazingStory/Internals/Pages/IFrame.razor | 16 +------- BlazingStory/Internals/Services/JSModule.cs | 6 +-- 11 files changed, 92 insertions(+), 122 deletions(-) diff --git a/BlazingStory/Components/BlazingStoryApp.razor b/BlazingStory/Components/BlazingStoryApp.razor index b983e5cd..5010eca7 100644 --- a/BlazingStory/Components/BlazingStoryApp.razor +++ b/BlazingStory/Components/BlazingStoryApp.razor @@ -140,7 +140,7 @@ private string _PreferesColorScheme = "light"; - private int _PreferesColorSchemeChangeSubscribeId = 0; + private IJSObjectReference? _PreferesColorSchemeChangeSubscriber; private readonly BlazingStoryOptions _Options = new(); @@ -208,7 +208,7 @@ await this.UpdatePreferesColorSchemeAsync(); - this._PreferesColorSchemeChangeSubscribeId = await this._JSModule.InvokeAsync("subscribePreferesColorSchemeChanged", this._RefThis, nameof(OnPreferesColorSchemeChanged)); + this._PreferesColorSchemeChangeSubscriber = await this._JSModule.InvokeAsync("subscribePreferesColorSchemeChanged", this._RefThis, nameof(OnPreferesColorSchemeChanged)); // Init Level 0 -> Default Background is visible & Color Scheme Container is hidden and transparent. @@ -265,11 +265,7 @@ public async ValueTask DisposeAsync() { - if (this._PreferesColorSchemeChangeSubscribeId != 0) - { - await this._JSModule.InvokeVoidIfConnectedAsync("unsubscribePreferesColorSchemeChanged", this._PreferesColorSchemeChangeSubscribeId); - this._PreferesColorSchemeChangeSubscribeId = 0; - } + await this._PreferesColorSchemeChangeSubscriber.DisposeIfConnectedAsync("dispose"); this._RefThis.Dispose(); await _JSModule.DisposeAsync(); } diff --git a/BlazingStory/Components/BlazingStoryApp.razor.js b/BlazingStory/Components/BlazingStoryApp.razor.js index 8591319c..e8506cff 100644 --- a/BlazingStory/Components/BlazingStoryApp.razor.js +++ b/BlazingStory/Components/BlazingStoryApp.razor.js @@ -19,21 +19,8 @@ export const ensureAllFontsAndStylesAreLoaded = async () => { }; const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); export const getPrefersColorScheme = () => darkModeMediaQuery.matches ? "dark" : "light"; -let subscriberIndex = 0; -const subscribers = new Map(); export const subscribePreferesColorSchemeChanged = (dotnetObjRef, methodName) => { - const subscriber = (e) => { - dotnetObjRef.invokeMethodAsync(methodName, getPrefersColorScheme()); - }; + const subscriber = (e) => { dotnetObjRef.invokeMethodAsync(methodName, getPrefersColorScheme()); }; darkModeMediaQuery.addEventListener("change", subscriber); - subscriberIndex++; - subscribers.set(subscriberIndex, subscriber); - return subscriberIndex; -}; -export const unsubscribePreferesColorSchemeChanged = (subscriberIndex) => { - const subscriber = subscribers.get(subscriberIndex); - if (typeof (subscriber) === "undefined") - return; - darkModeMediaQuery.removeEventListener("change", subscriber); - subscribers.delete(subscriberIndex); + return ({ dispose: () => darkModeMediaQuery.removeEventListener("change", subscriber) }); }; diff --git a/BlazingStory/Components/BlazingStoryApp.razor.ts b/BlazingStory/Components/BlazingStoryApp.razor.ts index 27b7ea29..41d29999 100644 --- a/BlazingStory/Components/BlazingStoryApp.razor.ts +++ b/BlazingStory/Components/BlazingStoryApp.razor.ts @@ -1,4 +1,4 @@ -import { DotNetObjectReference } from "../Scripts/types"; +import { DotNetObjectReference, IDisposable } from "../Scripts/types"; export const ensureAllFontsAndStylesAreLoaded = async () => { @@ -26,23 +26,8 @@ const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); export const getPrefersColorScheme = (): string => darkModeMediaQuery.matches ? "dark" : "light"; -let subscriberIndex = 0; -const subscribers = new Map void>(); - -export const subscribePreferesColorSchemeChanged = (dotnetObjRef: DotNetObjectReference, methodName: string): number => { - const subscriber = (e: MediaQueryListEvent) => { - dotnetObjRef.invokeMethodAsync(methodName, getPrefersColorScheme()); - }; +export const subscribePreferesColorSchemeChanged = (dotnetObjRef: DotNetObjectReference, methodName: string): IDisposable => { + const subscriber = (e: MediaQueryListEvent) => { dotnetObjRef.invokeMethodAsync(methodName, getPrefersColorScheme()); }; darkModeMediaQuery.addEventListener("change", subscriber); - - subscriberIndex++; - subscribers.set(subscriberIndex, subscriber); - return subscriberIndex; -} - -export const unsubscribePreferesColorSchemeChanged = (subscriberIndex: number): void => { - const subscriber = subscribers.get(subscriberIndex); - if (typeof (subscriber) === "undefined") return; - darkModeMediaQuery.removeEventListener("change", subscriber); - subscribers.delete(subscriberIndex); + return ({ dispose: () => darkModeMediaQuery.removeEventListener("change", subscriber) }); } \ No newline at end of file diff --git a/BlazingStory/Internals/Components/Menus/PopupMenu.razor b/BlazingStory/Internals/Components/Menus/PopupMenu.razor index f4e04c6f..9e639dfa 100644 --- a/BlazingStory/Internals/Components/Menus/PopupMenu.razor +++ b/BlazingStory/Internals/Components/Menus/PopupMenu.razor @@ -86,9 +86,7 @@ this._HotKeysContext?.Dispose(); this._HotKeysContext = null; - if (this._EventSubscriber == null) return; - await this._EventSubscriber.InvokeVoidIfConnectedAsync("dispose"); - await this._EventSubscriber.DisposeIfConnectedAsync(); + await this._EventSubscriber.DisposeIfConnectedAsync("dispose"); this._EventSubscriber = null; } diff --git a/BlazingStory/Internals/Components/Preview/PreviewFrame.razor b/BlazingStory/Internals/Components/Preview/PreviewFrame.razor index a48c17ee..dfeef612 100644 --- a/BlazingStory/Internals/Components/Preview/PreviewFrame.razor +++ b/BlazingStory/Internals/Components/Preview/PreviewFrame.razor @@ -159,8 +159,7 @@ public async ValueTask DisposeAsync() { - await _EventMonitorSubscriber.InvokeVoidIfConnectedAsync("dispose"); - await _EventMonitorSubscriber.DisposeIfConnectedAsync(); + await _EventMonitorSubscriber.DisposeIfConnectedAsync("dispose"); await _JSModule.DisposeAsync(); _ThisRef?.Dispose(); } diff --git a/BlazingStory/Internals/Extensions/IJSExtensions.cs b/BlazingStory/Internals/Extensions/IJSExtensions.cs index a68557ea..50f69c46 100644 --- a/BlazingStory/Internals/Extensions/IJSExtensions.cs +++ b/BlazingStory/Internals/Extensions/IJSExtensions.cs @@ -1,18 +1,52 @@ -using Microsoft.JSInterop; +using BlazingStory.Internals.Utils; +using Microsoft.JSInterop; namespace BlazingStory.Internals.Extensions; internal static class IJSExtensions { + private const string _DefaultBasePath = "./_content/BlazingStory/"; + + /// + /// Import a JavScript module from the specified path. + /// + /// The instance. + /// The path to the JavaScript module to import, like "./Component.razor.js". + /// + public static async ValueTask ImportAsync(this IJSRuntime jsRuntime, string modulePath) + { + var updateToken = UriParameterKit.GetUpdateToken(jsRuntime); + return await jsRuntime.InvokeAsync("import", _DefaultBasePath + modulePath + updateToken); + } + + /// + /// Invoke a JavaScript function with the specified identifier.
+ /// This method will not throw an exception if the is disconnected. + ///
+ /// The instance. + /// An identifier for the function to invoke. + /// JSON-serializable arguments. public static async ValueTask InvokeVoidIfConnectedAsync(this IJSObjectReference? value, string identifier, params object?[]? args) { try { if (value != null) await value.InvokeVoidAsync(identifier, args); } catch (JSDisconnectedException) { } } - public static async ValueTask DisposeIfConnectedAsync(this IJSObjectReference? value) + /// + /// Dispose this object.
+ /// If is not null, it will be invoked before disposing the object.
+ /// This method will not throw an exception if the is disconnected. + ///
+ /// The instance. + /// If specified, this method will be invoked before disposing the object. + public static async ValueTask DisposeIfConnectedAsync(this IJSObjectReference? value, string? methodToCallBeforeDispose = null) { - try { if (value != null) await value.DisposeAsync(); } + if (value == null) return; + try + { + if (methodToCallBeforeDispose != null) await value.InvokeVoidAsync(methodToCallBeforeDispose); + await value.DisposeAsync(); + } catch (JSDisconnectedException) { } } } diff --git a/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor b/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor index af2a01f9..10a7257d 100644 --- a/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor +++ b/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor @@ -69,9 +69,9 @@ "opacity:1 !important;" + "transition:none !important;"; - private readonly DotNetObjectReference _This; + private DotNetObjectReference? _This; - private readonly JSModule _JSModule; + private IJSObjectReference? _EventMonitorSubscriber; public struct DOMRect { @@ -129,16 +129,13 @@ private string _ContentSizeText = ""; - public MeasureLayer() - { - this._This = DotNetObjectReference.Create(this); - this._JSModule = new(() => this.JSRuntime, "Internals/Pages/Canvas/MeasureLayer.razor.js"); - } - protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; - await this._JSModule.InvokeVoidAsync("attach", this._This); + + await using var module = await this.JSRuntime.ImportAsync("Internals/Pages/Canvas/MeasureLayer.razor.js"); + this._This = DotNetObjectReference.Create(this); + this._EventMonitorSubscriber = await module.InvokeAsync("subscribeTargetElementChanged", this._This); } [JSInvokable(nameof(TargetElementChanged))] @@ -315,8 +312,7 @@ public async ValueTask DisposeAsync() { - await this._JSModule.InvokeVoidIfConnectedAsync("detach"); - await this._JSModule.DisposeAsync(); - this._This.Dispose(); + await this._EventMonitorSubscriber.DisposeIfConnectedAsync("dispose"); + this._This?.Dispose(); } } \ No newline at end of file diff --git a/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.js b/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.js index ed6071ac..e9a74b42 100644 --- a/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.js +++ b/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.js @@ -5,27 +5,23 @@ const targetEvents = [ "pointerleave", "scroll" ]; -let lastHoveredElement = null; -let attachedOwner = null; const pxToNumber = (px) => parseInt(px.replace("px", ""), 10); const getSpacingSize = (style, prefix) => { const [top, left, bottom, right] = ["Top", "Left", "Bottom", "Right"].map(sufix => pxToNumber(style[prefix + sufix])); return { top, left, bottom, right }; }; -const handler = (ev) => { - if (attachedOwner === null) - return; +const handler = (context, ev) => { let hoveredElement = (ev instanceof MouseEvent) && document.elementFromPoint(ev.clientX, ev.clientY); let measurement = null; - if (lastHoveredElement !== null && hoveredElement === null) { - lastHoveredElement = null; + if (context.lastHoveredElement !== null && hoveredElement === null) { + context.lastHoveredElement = null; } - else if (hoveredElement !== null && lastHoveredElement !== hoveredElement) { - lastHoveredElement = hoveredElement === false ? lastHoveredElement : hoveredElement; - if (lastHoveredElement !== null) { - const computedStyle = window.getComputedStyle(lastHoveredElement); + else if (hoveredElement !== null && context.lastHoveredElement !== hoveredElement) { + context.lastHoveredElement = hoveredElement === false ? context.lastHoveredElement : hoveredElement; + if (context.lastHoveredElement !== null) { + const computedStyle = window.getComputedStyle(context.lastHoveredElement); measurement = { - boundary: lastHoveredElement.getBoundingClientRect(), + boundary: context.lastHoveredElement.getBoundingClientRect(), padding: getSpacingSize(computedStyle, "padding"), margin: getSpacingSize(computedStyle, "margin"), }; @@ -33,13 +29,11 @@ const handler = (ev) => { } else return; - attachedOwner.invokeMethodAsync("TargetElementChanged", measurement); -}; -export const attach = (owner) => { - attachedOwner = owner; - targetEvents.forEach(eventName => doc.addEventListener(eventName, handler)); + context.owner.invokeMethodAsync("TargetElementChanged", measurement); }; -export const detach = () => { - attachedOwner = null; - targetEvents.forEach(eventName => doc.removeEventListener(eventName, handler)); +export const subscribeTargetElementChanged = (owner) => { + const context = { owner, lastHoveredElement: null }; + const h = (ev) => handler(context, ev); + targetEvents.forEach(eventName => doc.addEventListener(eventName, h)); + return ({ dispose: () => { targetEvents.forEach(eventName => doc.removeEventListener(eventName, h)); } }); }; diff --git a/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.ts b/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.ts index 65b4f0d3..12596243 100644 --- a/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.ts +++ b/BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.ts @@ -1,4 +1,4 @@ -import { DotNetObjectReference } from "../../../Scripts/types"; +import { DotNetObjectReference, IDisposable } from "../../../Scripts/types"; type SpacingSize = { top: number; @@ -21,9 +21,10 @@ const targetEvents = [ "scroll" ] as const; -let lastHoveredElement: HTMLElement | null = null; - -let attachedOwner: DotNetObjectReference | null = null; +type Context = { + owner: DotNetObjectReference, + lastHoveredElement: HTMLElement | null +}; const pxToNumber = (px: string) => parseInt(px.replace("px", ""), 10); @@ -32,21 +33,20 @@ const getSpacingSize = (style: CSSStyleDeclaration, prefix: "margin" | "padding" return { top, left, bottom, right }; } -const handler = (ev: MouseEvent | Event) => { - if (attachedOwner === null) return; +const handler = (context: Context, ev: MouseEvent | Event) => { let hoveredElement = (ev instanceof MouseEvent) && document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement | null; let measurement: Measurement | null = null; - if (lastHoveredElement !== null && hoveredElement === null) { - lastHoveredElement = null; + if (context.lastHoveredElement !== null && hoveredElement === null) { + context.lastHoveredElement = null; } - else if (hoveredElement !== null && lastHoveredElement !== hoveredElement) { - lastHoveredElement = hoveredElement === false ? lastHoveredElement : hoveredElement; - if (lastHoveredElement !== null) { - const computedStyle = window.getComputedStyle(lastHoveredElement); + else if (hoveredElement !== null && context.lastHoveredElement !== hoveredElement) { + context.lastHoveredElement = hoveredElement === false ? context.lastHoveredElement : hoveredElement; + if (context.lastHoveredElement !== null) { + const computedStyle = window.getComputedStyle(context.lastHoveredElement); measurement = { - boundary: lastHoveredElement.getBoundingClientRect(), + boundary: context.lastHoveredElement.getBoundingClientRect(), padding: getSpacingSize(computedStyle, "padding"), margin: getSpacingSize(computedStyle, "margin"), }; @@ -54,15 +54,12 @@ const handler = (ev: MouseEvent | Event) => { } else return; - attachedOwner.invokeMethodAsync("TargetElementChanged", measurement); + context.owner.invokeMethodAsync("TargetElementChanged", measurement); } -export const attach = (owner: DotNetObjectReference) => { - attachedOwner = owner; - targetEvents.forEach(eventName => doc.addEventListener(eventName, handler)); +export const subscribeTargetElementChanged = (owner: DotNetObjectReference): IDisposable => { + const context = { owner, lastHoveredElement: null as (HTMLElement | null)}; + const h = (ev: MouseEvent | Event) => handler(context, ev); + targetEvents.forEach(eventName => doc.addEventListener(eventName, h)); + return ({ dispose: () => { targetEvents.forEach(eventName => doc.removeEventListener(eventName, h)); } }); } - -export const detach = () => { - attachedOwner = null; - targetEvents.forEach(eventName => doc.removeEventListener(eventName, handler)); -} \ No newline at end of file diff --git a/BlazingStory/Internals/Pages/IFrame.razor b/BlazingStory/Internals/Pages/IFrame.razor index cf918a2b..7220f547 100644 --- a/BlazingStory/Internals/Pages/IFrame.razor +++ b/BlazingStory/Internals/Pages/IFrame.razor @@ -1,6 +1,5 @@ @page "/iframe.html" @layout NullLayout -@implements IAsyncDisposable @inject IJSRuntime JSRuntime @@ -9,21 +8,10 @@ @code { - private readonly JSModule _JSModule; - - public IFrame() - { - this._JSModule = new(() => this.JSRuntime, "Internals/Pages/IFrame.razor.js"); - } - protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; - await this._JSModule.InvokeVoidAsync("initializeCanvasFrame"); - } - - public async ValueTask DisposeAsync() - { - await this._JSModule.DisposeAsync(); + await using var module = await this.JSRuntime.ImportAsync("Internals/Pages/IFrame.razor.js"); + await module.InvokeVoidAsync("initializeCanvasFrame"); } } \ No newline at end of file diff --git a/BlazingStory/Internals/Services/JSModule.cs b/BlazingStory/Internals/Services/JSModule.cs index 29350978..e03108fa 100644 --- a/BlazingStory/Internals/Services/JSModule.cs +++ b/BlazingStory/Internals/Services/JSModule.cs @@ -1,5 +1,4 @@ using BlazingStory.Internals.Extensions; -using BlazingStory.Internals.Utils; using Microsoft.JSInterop; namespace BlazingStory.Internals.Services; @@ -12,8 +11,6 @@ internal class JSModule : IAsyncDisposable private readonly string _ModulePath; - private const string _DefaultBasePath = "./_content/BlazingStory/"; - internal JSModule(Func jSRuntime, string modulePath) { this._GetJSRuntime = jSRuntime; @@ -25,8 +22,7 @@ private async ValueTask GetModuleAsync() if (this._Module == null) { var jsRuntime = this._GetJSRuntime(); - var updateToken = UriParameterKit.GetUpdateToken(jsRuntime); - this._Module = await jsRuntime.InvokeAsync("import", _DefaultBasePath + this._ModulePath + updateToken); + this._Module = await jsRuntime.ImportAsync(this._ModulePath); } return this._Module; }