From de82942a5c0a9f94dfaf042e0679882708664903 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 30 Mar 2026 11:01:03 +0200 Subject: [PATCH 1/2] Revert "[blazor][wasm] JSExport for events (#65897)" This reverts commit 41822eeedb4a240e411a6b54d50585f9b7674d28. Keep new tests --- .../Web.JS/src/Boot.WebAssembly.Common.ts | 27 +- src/Components/Web.JS/src/GlobalExports.ts | 21 +- .../Rendering/WebRendererInteropMethods.ts | 194 +----- .../src/Hosting/WebAssemblyHost.cs | 4 + .../src/Hosting/WebAssemblyHostBuilder.cs | 3 + .../src/Infrastructure/JSInteropMethods.cs | 32 + .../WebAssembly/src/PublicAPI.Unshipped.txt | 3 - .../src/Rendering/WebAssemblyRenderer.cs | 6 - ...faultWebAssemblyJSRuntime.EventDispatch.cs | 558 ------------------ .../DefaultWebAssemblyJSRuntime.Interop.cs | 112 ---- .../Services/DefaultWebAssemblyJSRuntime.cs | 98 ++- 11 files changed, 140 insertions(+), 918 deletions(-) delete mode 100644 src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.EventDispatch.cs delete mode 100644 src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.Interop.cs diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts b/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts index fe9b884e774f..46311e7f1c8f 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts @@ -121,16 +121,25 @@ async function startCore(components: RootComponentManager { - return Blazor._internal.dotNetExports!.DispatchLocationChanged(uri, state ?? null, intercepted); - } + Blazor._internal.navigationManager.listenForNavigationEvents(WebRendererId.WebAssembly, async (uri: string, state: string | undefined, intercepted: boolean): Promise => { + await dispatcher.invokeDotNetStaticMethodAsync( + 'Microsoft.AspNetCore.Components.WebAssembly', + 'NotifyLocationChanged', + uri, + state, + intercepted + ); + }, async (callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise => { + const shouldContinueNavigation = await dispatcher.invokeDotNetStaticMethodAsync( + 'Microsoft.AspNetCore.Components.WebAssembly', + 'NotifyLocationChangingAsync', + uri, + state, + intercepted + ); - async function dispatchLocationChanging(callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise { - const shouldContinueNavigation = await Blazor._internal.dotNetExports!.DispatchLocationChanging(uri, state ?? null, intercepted); Blazor._internal.navigationManager.endLocationChanging(callId, shouldContinueNavigation); - } - - Blazor._internal.navigationManager.listenForNavigationEvents(WebRendererId.WebAssembly, dispatchLocationChanged, dispatchLocationChanging); + }); // Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on // the document. @@ -148,7 +157,7 @@ async function startCore(components: RootComponentManager initialUpdatePromise; Blazor._internal.updateRootComponents = (operations: string, webAssemblyState: string) => { - Blazor._internal.dotNetExports?.UpdateRootComponents(operations, webAssemblyState); + Blazor._internal.dotNetExports?.UpdateRootComponentsCore(operations, webAssemblyState); }; Blazor._internal.endUpdateRootComponents = (batchId: number) => diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts index 3b03de0a0f9f..6bd2fbe69c75 100644 --- a/src/Components/Web.JS/src/GlobalExports.ts +++ b/src/Components/Web.JS/src/GlobalExports.ts @@ -92,26 +92,7 @@ export interface IBlazor { EndInvokeJS: (argsJson: string) => void; BeginInvokeDotNet: (callId: string | null, assemblyNameOrDotNetObjectId: string, methodIdentifier: string, argsJson: string) => void; ReceiveByteArrayFromJS: (id: number, data: Uint8Array) => void; - UpdateRootComponents: (operationsJson: string, appState: string) => Promise; - - // Event dispatch fast paths (bypass JSON serialization + DotNetDispatcher) - DispatchMouseEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, detail: number, screenX: number, screenY: number, clientX: number, clientY: number, offsetX: number, offsetY: number, pageX: number, pageY: number, movementX: number, movementY: number, button: number, buttons: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, type: string) => void; - DispatchDragEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, detail: number, screenX: number, screenY: number, clientX: number, clientY: number, offsetX: number, offsetY: number, pageX: number, pageY: number, movementX: number, movementY: number, button: number, buttons: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, type: string, dropEffect: string | null, effectAllowed: string | null, files: string[] | null, itemKinds: string[] | null, itemTypes: string[] | null, types: string[] | null) => void; - DispatchKeyboardEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, key: string, code: string, location: number, repeat: boolean, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, type: string, isComposing: boolean) => void; - DispatchChangeEventString: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, value: string) => void; - DispatchChangeEventBool: (eventHandlerId: number, fieldComponentId: number, fieldValueBool: boolean, value: boolean) => void; - DispatchChangeEventStringArray: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, value: (string | null)[]) => void; - DispatchFocusEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, type: string | null) => void; - DispatchClipboardEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, type: string) => void; - DispatchPointerEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, detail: number, screenX: number, screenY: number, clientX: number, clientY: number, offsetX: number, offsetY: number, pageX: number, pageY: number, movementX: number, movementY: number, button: number, buttons: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, type: string, pointerId: number, width: number, height: number, pressure: number, tiltX: number, tiltY: number, pointerType: string, isPrimary: boolean) => void; - DispatchWheelEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, detail: number, screenX: number, screenY: number, clientX: number, clientY: number, offsetX: number, offsetY: number, pageX: number, pageY: number, movementX: number, movementY: number, button: number, buttons: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, type: string, deltaX: number, deltaY: number, deltaZ: number, deltaMode: number) => void; - DispatchTouchEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, detail: number, touchesFlat: number[] | null, targetTouchesFlat: number[] | null, changedTouchesFlat: number[] | null, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, type: string) => void; - DispatchProgressEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, lengthComputable: boolean, loaded: number, total: number, type: string) => void; - DispatchErrorEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, message: string | null, filename: string | null, lineno: number, colno: number, type: string | null) => void; - DispatchEmptyEvent: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean) => void; - DispatchEventJson: (eventHandlerId: number, fieldComponentId: number, fieldValueString: string | null, fieldValueBool: boolean, eventName: string, eventArgsJson: string) => void; - DispatchLocationChanged: (uri: string, state: string | null, isInterceptedLink: boolean) => Promise; - DispatchLocationChanging: (uri: string, state: string | null, isInterceptedLink: boolean) => Promise; + UpdateRootComponentsCore: (operationsJson: string, appState: string) => void; } // APIs invoked by hot reload diff --git a/src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts b/src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts index c56b02c272bd..d8b20b5a6b81 100644 --- a/src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts +++ b/src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts @@ -4,8 +4,6 @@ import { DotNet } from '@microsoft/dotnet-js-interop'; import { EventDescriptor } from './Events/EventDelegator'; import { enableJSRootComponents, JSComponentParametersByIdentifier, JSComponentIdentifiersByInitializer } from './JSRootComponents'; -import { Blazor } from '../GlobalExports'; -import { WebRendererId } from './WebRendererId'; const interopMethodsByRenderer = new Map(); const rendererAttachedListeners: ((browserRendererId: number) => void)[] = []; @@ -67,202 +65,14 @@ function invokeRendererAttachedListeners(browserRendererId: number) { export function dispatchEvent(browserRendererId: number, eventDescriptor: EventDescriptor, eventArgs: any): void { return dispatchEventMiddleware(browserRendererId, eventDescriptor.eventHandlerId, () => { - const exports = Blazor._internal.dotNetExports; - if (exports && browserRendererId === WebRendererId.WebAssembly) { - dispatchEventDirect(exports, eventDescriptor, eventArgs); - return; - } const interopMethods = getInteropMethods(browserRendererId); return interopMethods.invokeMethodAsync('DispatchEventAsync', eventDescriptor, eventArgs); }); } -function dispatchEventDirect( - exports: NonNullable, - eventDescriptor: EventDescriptor, - eventArgs: any, -): void { - const { eventHandlerId, eventFieldInfo } = eventDescriptor; - const fieldComponentId = eventFieldInfo ? eventFieldInfo.componentId : -1; - const fieldValueString = eventFieldInfo && typeof eventFieldInfo.fieldValue === 'string' ? eventFieldInfo.fieldValue : null; - const fieldValueBool = eventFieldInfo && typeof eventFieldInfo.fieldValue === 'boolean' ? eventFieldInfo.fieldValue : false; - - switch (eventDescriptor.eventName) { - case 'click': case 'mousedown': case 'mouseup': case 'dblclick': - case 'contextmenu': case 'mouseover': case 'mouseout': case 'mousemove': - case 'mouseleave': case 'mouseenter': - exports.DispatchMouseEvent( - eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, - eventArgs.detail, eventArgs.screenX, eventArgs.screenY, - eventArgs.clientX, eventArgs.clientY, eventArgs.offsetX, eventArgs.offsetY, - eventArgs.pageX, eventArgs.pageY, eventArgs.movementX, eventArgs.movementY, - eventArgs.button, eventArgs.buttons, - eventArgs.ctrlKey, eventArgs.shiftKey, eventArgs.altKey, eventArgs.metaKey, - eventArgs.type - ); - return; - - case 'drag': case 'dragend': case 'dragenter': case 'dragleave': - case 'dragover': case 'dragstart': case 'drop': { - const dt = eventArgs.dataTransfer; - const itemKinds = dt?.items ? dt.items.map((i: any) => i.kind) : null; - const itemTypes = dt?.items ? dt.items.map((i: any) => i.type) : null; - exports.DispatchDragEvent( - eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, - eventArgs.detail, eventArgs.screenX, eventArgs.screenY, - eventArgs.clientX, eventArgs.clientY, eventArgs.offsetX, eventArgs.offsetY, - eventArgs.pageX, eventArgs.pageY, eventArgs.movementX, eventArgs.movementY, - eventArgs.button, eventArgs.buttons, - eventArgs.ctrlKey, eventArgs.shiftKey, eventArgs.altKey, eventArgs.metaKey, - eventArgs.type, - dt?.dropEffect ?? null, dt?.effectAllowed ?? null, - dt?.files ?? null, - itemKinds, itemTypes, - dt?.types ?? null - ); - return; - } - - case 'keydown': case 'keyup': case 'keypress': - exports.DispatchKeyboardEvent( - eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, - eventArgs.key, eventArgs.code, eventArgs.location, - eventArgs.repeat, eventArgs.ctrlKey, eventArgs.shiftKey, - eventArgs.altKey, eventArgs.metaKey, eventArgs.type, eventArgs.isComposing - ); - return; - - case 'input': case 'change': - if (typeof eventArgs.value === 'boolean') { - exports.DispatchChangeEventBool(eventHandlerId, fieldComponentId, fieldValueBool, eventArgs.value); - return; - } else if (typeof eventArgs.value === 'string') { - exports.DispatchChangeEventString(eventHandlerId, fieldComponentId, fieldValueString, eventArgs.value); - return; - } else if (Array.isArray(eventArgs.value)) { - exports.DispatchChangeEventStringArray(eventHandlerId, fieldComponentId, fieldValueString, eventArgs.value); - return; - } - break; - - case 'focus': case 'blur': case 'focusin': case 'focusout': - exports.DispatchFocusEvent(eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, eventArgs.type); - return; - - case 'copy': case 'cut': case 'paste': - exports.DispatchClipboardEvent(eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, eventArgs.type); - return; - - case 'gotpointercapture': case 'lostpointercapture': case 'pointercancel': - case 'pointerdown': case 'pointerenter': case 'pointerleave': - case 'pointermove': case 'pointerout': case 'pointerover': case 'pointerup': - exports.DispatchPointerEvent( - eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, - eventArgs.detail, eventArgs.screenX, eventArgs.screenY, - eventArgs.clientX, eventArgs.clientY, eventArgs.offsetX, eventArgs.offsetY, - eventArgs.pageX, eventArgs.pageY, eventArgs.movementX, eventArgs.movementY, - eventArgs.button, eventArgs.buttons, - eventArgs.ctrlKey, eventArgs.shiftKey, eventArgs.altKey, eventArgs.metaKey, - eventArgs.type, - eventArgs.pointerId, eventArgs.width, eventArgs.height, - eventArgs.pressure, eventArgs.tiltX, eventArgs.tiltY, - eventArgs.pointerType, eventArgs.isPrimary - ); - return; - - case 'wheel': case 'mousewheel': - exports.DispatchWheelEvent( - eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, - eventArgs.detail, eventArgs.screenX, eventArgs.screenY, - eventArgs.clientX, eventArgs.clientY, eventArgs.offsetX, eventArgs.offsetY, - eventArgs.pageX, eventArgs.pageY, eventArgs.movementX, eventArgs.movementY, - eventArgs.button, eventArgs.buttons, - eventArgs.ctrlKey, eventArgs.shiftKey, eventArgs.altKey, eventArgs.metaKey, - eventArgs.type, - eventArgs.deltaX, eventArgs.deltaY, eventArgs.deltaZ, eventArgs.deltaMode); - return; - - case 'touchcancel': case 'touchend': case 'touchmove': - case 'touchenter': case 'touchleave': case 'touchstart': - exports.DispatchTouchEvent( - eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, - eventArgs.detail, - flattenTouchList(eventArgs.touches), - flattenTouchList(eventArgs.targetTouches), - flattenTouchList(eventArgs.changedTouches), - eventArgs.ctrlKey, eventArgs.shiftKey, eventArgs.altKey, eventArgs.metaKey, - eventArgs.type - ); - return; - - case 'loadstart': case 'timeout': case 'abort': - case 'load': case 'loadend': case 'progress': - exports.DispatchProgressEvent( - eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, - eventArgs.lengthComputable, eventArgs.loaded, eventArgs.total, eventArgs.type - ); - return; - - case 'error': - exports.DispatchErrorEvent( - eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, - eventArgs.message, eventArgs.filename, eventArgs.lineno, eventArgs.colno, eventArgs.type - ); - return; - - case 'cancel': case 'close': case 'submit': case 'toggle': - exports.DispatchEmptyEvent(eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool); - return; - - default: - break; // drag events, custom events → fall through to JSON fallback - } - - // Fallback: serialize remaining event types as JSON via JSExport - // Use interop-aware serializer so DotNetObjectReference, Uint8Array, etc. are properly encoded - let nextByteArrayId = 0; - const json = JSON.stringify(eventArgs, (_key, value) => { - if (value instanceof DotNet.DotNetObject) { - return value.serializeAsArg(); - } - if (value instanceof Uint8Array) { - const id = nextByteArrayId++; - exports.ReceiveByteArrayFromJS(id, value); - return { ['__byte[]']: id }; - } - return value; - }); - exports.DispatchEventJson(eventHandlerId, fieldComponentId, fieldValueString, fieldValueBool, eventDescriptor.eventName, json); -} - -function flattenTouchList(touchPoints: any[] | undefined): number[] | null { - if (!touchPoints || touchPoints.length === 0) { - return null; - } - const count = touchPoints.length; - const result: number[] = new Array(count * 7); - for (let i = 0; i < count; i++) { - const p = touchPoints[i]; - const o = i * 7; - result[o] = p.identifier; - result[o + 1] = p.screenX; - result[o + 2] = p.screenY; - result[o + 3] = p.clientX; - result[o + 4] = p.clientY; - result[o + 5] = p.pageX; - result[o + 6] = p.pageY; - } - return result; -} - -export function updateRootComponents(browserRendererId: number, operationsJson: string): void { - const exports = Blazor._internal.dotNetExports; - if (exports && browserRendererId === WebRendererId.WebAssembly) { - exports.UpdateRootComponents(operationsJson, ''); - return; - } +export function updateRootComponents(browserRendererId: number, operationsJson: string): Promise { const interopMethods = getInteropMethods(browserRendererId); - interopMethods.invokeMethodAsync('UpdateRootComponents', operationsJson); + return interopMethods.invokeMethodAsync('UpdateRootComponents', operationsJson); } function getInteropMethods(rendererId: number): DotNet.DotNetObject { diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index c73089eb3e7d..9ba493f76a46 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Infrastructure; +using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Rendering; using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.Configuration; @@ -45,6 +46,9 @@ internal WebAssemblyHost( AsyncServiceScope scope, string? persistedState) { + // To ensure JS-invoked methods don't get linked out, have a reference to their enclosing types + GC.KeepAlive(typeof(JSInteropMethods)); + _services = services; _scope = scope; _configuration = builder.Configuration; diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 6110d24d56aa..58eee4871b1c 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Rendering; using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.Configuration; @@ -38,6 +39,8 @@ public sealed class WebAssemblyHostBuilder /// /// The argument passed to the application's main method. /// A . + [DynamicDependency(nameof(JSInteropMethods.NotifyLocationChanged), typeof(JSInteropMethods))] + [DynamicDependency(nameof(JSInteropMethods.NotifyLocationChangingAsync), typeof(JSInteropMethods))] [DynamicDependency(JsonSerialized, typeof(WebEventDescriptor))] // The following dependency prevents HeadOutlet from getting trimmed away in // WebAssembly prerendered apps. diff --git a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs index 53616c226961..fffc8dd15a18 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs @@ -1,2 +1,34 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using Microsoft.AspNetCore.Components.WebAssembly.Services; +using Microsoft.JSInterop; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; + +/// +/// Contains methods called by interop. Intended for framework use only, not supported for use in application +/// code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class JSInteropMethods +{ + /// + /// For framework use only. + /// + [JSInvokable(nameof(NotifyLocationChanged))] + public static void NotifyLocationChanged(string uri, string? state, bool isInterceptedLink) + { + WebAssemblyNavigationManager.Instance.SetLocation(uri, state, isInterceptedLink); + } + + /// + /// For framework use only. + /// + [JSInvokable(nameof(NotifyLocationChangingAsync))] + public static async ValueTask NotifyLocationChangingAsync(string uri, string? state, bool isInterceptedLink) + { + return await WebAssemblyNavigationManager.Instance.HandleLocationChangingAsync(uri, state, isInterceptedLink); + } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt index 5877c61dfca5..c65885b63ac7 100644 --- a/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt +++ b/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt @@ -1,6 +1,3 @@ #nullable enable *REMOVED*~static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.NotifyLocationChanged(string uri, bool isInterceptedLink) -> void *REMOVED*static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.NotifyLocationChanged(string! uri, bool isInterceptedLink) -> void -*REMOVED*static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.NotifyLocationChanged(string! uri, string? state, bool isInterceptedLink) -> void -*REMOVED*static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.NotifyLocationChangingAsync(string! uri, string? state, bool isInterceptedLink) -> System.Threading.Tasks.ValueTask -*REMOVED*Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index eaa9d8c3dcb1..242e28ed7ba2 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -46,7 +46,6 @@ public WebAssemblyRenderer(IServiceProvider serviceProvider, ResourceAssetCollec ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext; DefaultWebAssemblyJSRuntime.Instance.OnUpdateRootComponents += OnUpdateRootComponents; - DefaultWebAssemblyJSRuntime.Instance.Renderer = this; } [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "These are root components which belong to the user and are in assemblies that don't get trimmed.")] @@ -132,11 +131,6 @@ protected override void AttachRootComponentToBrowser(int componentId, string dom /// protected override void Dispose(bool disposing) { - if (disposing) - { - DefaultWebAssemblyJSRuntime.Instance.OnUpdateRootComponents -= OnUpdateRootComponents; - DefaultWebAssemblyJSRuntime.Instance.Renderer = null; - } base.Dispose(disposing); } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.EventDispatch.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.EventDispatch.cs deleted file mode 100644 index 9cca37f6e491..000000000000 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.EventDispatch.cs +++ /dev/null @@ -1,558 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices.JavaScript; -using System.Runtime.Versioning; -using System.Text.Json; -using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; - -namespace Microsoft.AspNetCore.Components.WebAssembly.Services; - -internal sealed partial class DefaultWebAssemblyJSRuntime -{ - private static readonly object BoxedTrue = true; - private static readonly object BoxedFalse = false; - - internal Renderer? Renderer { get; set; } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static Task DispatchLocationChanged(string uri, string? state, bool isInterceptedLink) - { - return ScheduleOnCallQueue( - (uri, state, isInterceptedLink), - static s => WebAssemblyNavigationManager.Instance.SetLocation(s.uri, s.state, s.isInterceptedLink)); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static Task DispatchLocationChanging(string uri, string? state, bool isInterceptedLink) - { - return ScheduleOnCallQueue<(string uri, string? state, bool isInterceptedLink), bool>( - (uri, state, isInterceptedLink), - static s => WebAssemblyNavigationManager.Instance.HandleLocationChangingAsync(s.uri, s.state, s.isInterceptedLink).AsTask()); - } - - [SupportedOSPlatform("browser")] - [JSExport] - public static Task UpdateRootComponents(string operationsJson, string appState) - { - return ScheduleOnCallQueue((operationsJson, appState), static s => - { - try - { - var operations = DeserializeOperations(s.operationsJson); - Instance.OnUpdateRootComponents?.Invoke(operations, s.appState); - } - catch (Exception ex) - { - System.Console.Error.WriteLine($"Error in {nameof(UpdateRootComponents)}: {ex}"); - } - }); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchMouseEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - [JSMarshalAs] long detail, - double screenX, double screenY, - double clientX, double clientY, - double offsetX, double offsetY, - double pageX, double pageY, - double movementX, double movementY, - [JSMarshalAs] long button, - [JSMarshalAs] long buttons, - bool ctrlKey, bool shiftKey, bool altKey, bool metaKey, - string type) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new MouseEventArgs - { - Detail = detail, - ScreenX = screenX, ScreenY = screenY, - ClientX = clientX, ClientY = clientY, - OffsetX = offsetX, OffsetY = offsetY, - PageX = pageX, PageY = pageY, - MovementX = movementX, MovementY = movementY, - Button = button, Buttons = buttons, - CtrlKey = ctrlKey, ShiftKey = shiftKey, AltKey = altKey, MetaKey = metaKey, - Type = type, - }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchDragEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - [JSMarshalAs] long detail, - double screenX, double screenY, - double clientX, double clientY, - double offsetX, double offsetY, - double pageX, double pageY, - double movementX, double movementY, - [JSMarshalAs] long button, - [JSMarshalAs] long buttons, - bool ctrlKey, bool shiftKey, bool altKey, bool metaKey, - string type, - string? dropEffect, string? effectAllowed, - [JSMarshalAs>] string[]? files, - [JSMarshalAs>] string[]? itemKinds, - [JSMarshalAs>] string[]? itemTypes, - [JSMarshalAs>] string[]? types) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new DragEventArgs - { - Detail = detail, - ScreenX = screenX, ScreenY = screenY, - ClientX = clientX, ClientY = clientY, - OffsetX = offsetX, OffsetY = offsetY, - PageX = pageX, PageY = pageY, - MovementX = movementX, MovementY = movementY, - Button = button, Buttons = buttons, - CtrlKey = ctrlKey, ShiftKey = shiftKey, AltKey = altKey, MetaKey = metaKey, - Type = type, - DataTransfer = new DataTransfer - { - DropEffect = dropEffect ?? string.Empty, - EffectAllowed = effectAllowed, - Files = files ?? [], - Items = UnflattenDataTransferItems(itemKinds, itemTypes), - Types = types ?? [], - }, - }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchKeyboardEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - string key, string code, - float location, - bool repeat, - bool ctrlKey, bool shiftKey, bool altKey, bool metaKey, - string type, - bool isComposing) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new KeyboardEventArgs - { - Key = key, - Code = code, - Location = location, - Repeat = repeat, - CtrlKey = ctrlKey, ShiftKey = shiftKey, AltKey = altKey, MetaKey = metaKey, - Type = type, - IsComposing = isComposing, - }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchChangeEventString( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - string value) - { - EventFieldInfo? fieldInfo = fieldComponentId >= 0 - ? new EventFieldInfo { ComponentId = fieldComponentId, FieldValue = fieldValueString! } - : null; - var eventArgs = new ChangeEventArgs { Value = value }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchChangeEventBool( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - bool fieldValueBool, - bool value) - { - EventFieldInfo? fieldInfo = fieldComponentId >= 0 - ? new EventFieldInfo { ComponentId = fieldComponentId, FieldValue = fieldValueBool ? BoxedTrue : BoxedFalse } - : null; - var eventArgs = new ChangeEventArgs { Value = value }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchChangeEventStringArray( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - [JSMarshalAs>] string?[] value) - { - EventFieldInfo? fieldInfo = fieldComponentId >= 0 - ? new EventFieldInfo { ComponentId = fieldComponentId, FieldValue = fieldValueString! } - : null; - var eventArgs = new ChangeEventArgs { Value = value }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchFocusEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - string? type) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new FocusEventArgs { Type = type }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchClipboardEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - string type) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new ClipboardEventArgs { Type = type }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchPointerEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - [JSMarshalAs] long detail, - double screenX, double screenY, - double clientX, double clientY, - double offsetX, double offsetY, - double pageX, double pageY, - double movementX, double movementY, - [JSMarshalAs] long button, - [JSMarshalAs] long buttons, - bool ctrlKey, bool shiftKey, bool altKey, bool metaKey, - string type, - [JSMarshalAs] long pointerId, - float width, float height, - float pressure, - float tiltX, float tiltY, - string pointerType, - bool isPrimary) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new PointerEventArgs - { - Detail = detail, - ScreenX = screenX, ScreenY = screenY, - ClientX = clientX, ClientY = clientY, - OffsetX = offsetX, OffsetY = offsetY, - PageX = pageX, PageY = pageY, - MovementX = movementX, MovementY = movementY, - Button = button, Buttons = buttons, - CtrlKey = ctrlKey, ShiftKey = shiftKey, AltKey = altKey, MetaKey = metaKey, - Type = type, - PointerId = pointerId, - Width = width, Height = height, - Pressure = pressure, - TiltX = tiltX, TiltY = tiltY, - PointerType = pointerType, - IsPrimary = isPrimary, - }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchWheelEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - [JSMarshalAs] long detail, - double screenX, double screenY, - double clientX, double clientY, - double offsetX, double offsetY, - double pageX, double pageY, - double movementX, double movementY, - [JSMarshalAs] long button, - [JSMarshalAs] long buttons, - bool ctrlKey, bool shiftKey, bool altKey, bool metaKey, - string type, - double deltaX, double deltaY, double deltaZ, - [JSMarshalAs] long deltaMode) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new WheelEventArgs - { - Detail = detail, - ScreenX = screenX, ScreenY = screenY, - ClientX = clientX, ClientY = clientY, - OffsetX = offsetX, OffsetY = offsetY, - PageX = pageX, PageY = pageY, - MovementX = movementX, MovementY = movementY, - Button = button, Buttons = buttons, - CtrlKey = ctrlKey, ShiftKey = shiftKey, AltKey = altKey, MetaKey = metaKey, - Type = type, - DeltaX = deltaX, DeltaY = deltaY, DeltaZ = deltaZ, - DeltaMode = deltaMode, - }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchTouchEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - [JSMarshalAs] long detail, - [JSMarshalAs>] double[]? touchesFlat, - [JSMarshalAs>] double[]? targetTouchesFlat, - [JSMarshalAs>] double[]? changedTouchesFlat, - bool ctrlKey, bool shiftKey, bool altKey, bool metaKey, - string type) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new TouchEventArgs - { - Detail = detail, - Touches = UnflattenTouchPoints(touchesFlat), - TargetTouches = UnflattenTouchPoints(targetTouchesFlat), - ChangedTouches = UnflattenTouchPoints(changedTouchesFlat), - CtrlKey = ctrlKey, ShiftKey = shiftKey, AltKey = altKey, MetaKey = metaKey, - Type = type, - }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchProgressEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - bool lengthComputable, - [JSMarshalAs] long loaded, - [JSMarshalAs] long total, - string type) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new ProgressEventArgs - { - LengthComputable = lengthComputable, - Loaded = loaded, - Total = total, - Type = type, - }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchErrorEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - string? message, string? filename, - int lineno, int colno, - string? type) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var eventArgs = new Microsoft.AspNetCore.Components.Web.ErrorEventArgs - { - Message = message, - Filename = filename, - Lineno = lineno, - Colno = colno, - Type = type, - }; - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [JSExport] - [SupportedOSPlatform("browser")] - internal static void DispatchEmptyEvent( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - DispatchEventCore(eventHandlerId, fieldInfo, EventArgs.Empty); - } - - [JSExport] - [SupportedOSPlatform("browser")] - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", - Justification = "Custom event arg types are preserved by the component that declares the event handler.")] - internal static void DispatchEventJson( - [JSMarshalAs] long eventHandlerId, - int fieldComponentId, - string? fieldValueString, - bool fieldValueBool, - string eventName, - string eventArgsJson) - { - var fieldInfo = CreateFieldInfo(fieldComponentId, fieldValueString, fieldValueBool); - var options = Instance.ReadJsonSerializerOptions(); - var eventArgs = ParseEventArgs(eventHandlerId, eventName, eventArgsJson, options); - DispatchEventCore(eventHandlerId, fieldInfo, eventArgs); - } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", - Justification = "Custom event arg types are preserved by the component that declares the event handler.")] - private static EventArgs ParseEventArgs( - long eventHandlerId, - string eventName, - string eventArgsJson, - JsonSerializerOptions options) - { - return eventName switch - { - "drag" or "dragend" or "dragenter" or "dragleave" or "dragover" or "dragstart" or "drop" - => JsonSerializer.Deserialize(eventArgsJson, options)!, - - "input" or "change" - => ParseChangeEventArgs(eventArgsJson, options), - - _ => DeserializeCustomEventArgs(eventHandlerId, eventArgsJson, options), - }; - } - - internal static DataTransferItem[] UnflattenDataTransferItems(string[]? kinds, string[]? types) - { - if (kinds is null || kinds.Length == 0) - { - return []; - } - - var count = kinds.Length; - var result = new DataTransferItem[count]; - for (var i = 0; i < count; i++) - { - result[i] = new DataTransferItem - { - Kind = kinds[i], - Type = types is not null && i < types.Length ? types[i] : string.Empty, - }; - } - - return result; - } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", - Justification = "ChangeEventArgs is a well-known type that is preserved.")] - private static ChangeEventArgs ParseChangeEventArgs(string json, JsonSerializerOptions options) - { - using var document = JsonDocument.Parse(json); - var root = document.RootElement; - - if (root.TryGetProperty("value", out var valueElement) && valueElement.ValueKind == JsonValueKind.Array) - { - var length = valueElement.GetArrayLength(); - var values = new string?[length]; - var index = 0; - foreach (var item in valueElement.EnumerateArray()) - { - values[index++] = item.GetString(); - } - - return new ChangeEventArgs { Value = values }; - } - - // For non-array values, use standard deserialization - return JsonSerializer.Deserialize(json, options)!; - } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", - Justification = "Custom event arg types are preserved by the component that declares the event handler.")] - private static EventArgs DeserializeCustomEventArgs(long eventHandlerId, string eventArgsJson, JsonSerializerOptions options) - { - var renderer = Instance.Renderer; - if (renderer is null) - { - return EventArgs.Empty; - } - - var eventArgsType = renderer.GetEventArgsType((ulong)eventHandlerId); - - return (EventArgs)JsonSerializer.Deserialize(eventArgsJson, eventArgsType, options)!; - } - - internal static TouchPoint[] UnflattenTouchPoints(double[]? flat) - { - if (flat is null || flat.Length == 0) - { - return []; - } - - var count = flat.Length / 7; - var result = new TouchPoint[count]; - for (var i = 0; i < count; i++) - { - var offset = i * 7; - result[i] = new TouchPoint - { - Identifier = (long)flat[offset], - ScreenX = flat[offset + 1], - ScreenY = flat[offset + 2], - ClientX = flat[offset + 3], - ClientY = flat[offset + 4], - PageX = flat[offset + 5], - PageY = flat[offset + 6], - }; - } - - return result; - } - - internal static EventFieldInfo? CreateFieldInfo(int fieldComponentId, string? fieldValueString, bool fieldValueBool) - { - if (fieldComponentId < 0) - { - return null; - } - - return new EventFieldInfo - { - ComponentId = fieldComponentId, - FieldValue = fieldValueString is not null ? fieldValueString : (fieldValueBool ? BoxedTrue : BoxedFalse), - }; - } - - private static void DispatchEventCore(long eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs) - { - WebAssemblyCallQueue.Schedule((eventHandlerId, fieldInfo, eventArgs), static state => - { - var renderer = Instance.Renderer; - if (renderer is not null) - { - _ = renderer.DispatchEventAsync((ulong)state.eventHandlerId, state.fieldInfo, state.eventArgs); - } - }); - } -} diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.Interop.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.Interop.cs deleted file mode 100644 index 8839e84c365f..000000000000 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.Interop.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Globalization; -using System.Runtime.InteropServices.JavaScript; -using System.Runtime.Versioning; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Microsoft.JSInterop.Infrastructure; - -namespace Microsoft.AspNetCore.Components.WebAssembly.Services; - -internal sealed partial class DefaultWebAssemblyJSRuntime -{ - - [JSExport] - [SupportedOSPlatform("browser")] - public static string? InvokeDotNet( - string? assemblyName, - string methodIdentifier, - [JSMarshalAs] long dotNetObjectId, - string argsJson) - { - var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId: null); - return DotNetDispatcher.Invoke(Instance, callInfo, argsJson); - } - - [JSExport] - [SupportedOSPlatform("browser")] - public static void EndInvokeJS(string argsJson) - { - WebAssemblyCallQueue.Schedule(argsJson, static argsJson => - { - // This is not expected to throw, as it takes care of converting any unhandled user code - // exceptions into a failure on the Task that was returned when calling InvokeAsync. - DotNetDispatcher.EndInvokeJS(Instance, argsJson); - }); - } - - [JSExport] - [SupportedOSPlatform("browser")] - public static void BeginInvokeDotNet(string? callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) - { - // Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID - // We only need one for any given call. This helps to work around the limitation that we can - // only pass a maximum of 4 args in a call from JS to Mono WebAssembly. - string? assemblyName; - long dotNetObjectId; - if (char.IsDigit(assemblyNameOrDotNetObjectId[0])) - { - dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId, CultureInfo.InvariantCulture); - assemblyName = null; - } - else - { - dotNetObjectId = default; - assemblyName = assemblyNameOrDotNetObjectId; - } - - var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId); - WebAssemblyCallQueue.Schedule((callInfo, argsJson), static state => - { - // This is not expected to throw, as it takes care of converting any unhandled user code - // exceptions into a failure on the JS Promise object. - DotNetDispatcher.BeginInvokeDotNet(Instance, state.callInfo, state.argsJson); - }); - } - - [JSExport] - [SupportedOSPlatform("browser")] - private static void ReceiveByteArrayFromJS(int id, byte[] data) - { - DotNetDispatcher.ReceiveByteArray(Instance, id, data); - } - - private static Task ScheduleOnCallQueue(TState state, Action action) - { - var tcs = new TaskCompletionSource(); - WebAssemblyCallQueue.Schedule((tcs, state, action), static s => - { - try - { - s.action(s.state); - s.tcs.TrySetResult(); - } - catch (Exception ex) - { - s.tcs.TrySetException(ex); - } - }); - - return tcs.Task; - } - - private static Task ScheduleOnCallQueue(TState state, Func> action) - { - var tcs = new TaskCompletionSource(); - WebAssemblyCallQueue.Schedule((tcs, state, action), static async s => - { - try - { - var result = await s.action(s.state); - s.tcs.TrySetResult(result); - } - catch (Exception ex) - { - s.tcs.TrySetException(ex); - } - }); - - return tcs.Task; - } -} diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index 1f9d16191674..f55739f2bdda 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -2,8 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.Versioning; using System.Text.Json; using Microsoft.AspNetCore.Components.Web.Internal; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; @@ -26,24 +30,7 @@ internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime [DynamicDependency(nameof(EndInvokeJS))] [DynamicDependency(nameof(BeginInvokeDotNet))] [DynamicDependency(nameof(ReceiveByteArrayFromJS))] - [DynamicDependency(nameof(UpdateRootComponents))] - [DynamicDependency(nameof(DispatchMouseEvent))] - [DynamicDependency(nameof(DispatchDragEvent))] - [DynamicDependency(nameof(DispatchKeyboardEvent))] - [DynamicDependency(nameof(DispatchChangeEventString))] - [DynamicDependency(nameof(DispatchChangeEventBool))] - [DynamicDependency(nameof(DispatchChangeEventStringArray))] - [DynamicDependency(nameof(DispatchFocusEvent))] - [DynamicDependency(nameof(DispatchClipboardEvent))] - [DynamicDependency(nameof(DispatchPointerEvent))] - [DynamicDependency(nameof(DispatchWheelEvent))] - [DynamicDependency(nameof(DispatchTouchEvent))] - [DynamicDependency(nameof(DispatchProgressEvent))] - [DynamicDependency(nameof(DispatchErrorEvent))] - [DynamicDependency(nameof(DispatchEmptyEvent))] - [DynamicDependency(nameof(DispatchEventJson))] - [DynamicDependency(nameof(DispatchLocationChanged))] - [DynamicDependency(nameof(DispatchLocationChanging))] + [DynamicDependency(nameof(UpdateRootComponentsCore))] [DynamicDependency(JsonSerialized, typeof(KeyValuePair<,>))] private DefaultWebAssemblyJSRuntime() { @@ -53,6 +40,74 @@ private DefaultWebAssemblyJSRuntime() public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; + [JSExport] + [SupportedOSPlatform("browser")] + public static string? InvokeDotNet( + string? assemblyName, + string methodIdentifier, + [JSMarshalAs] long dotNetObjectId, + string argsJson) + { + var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId: null); + return DotNetDispatcher.Invoke(Instance, callInfo, argsJson); + } + + [JSExport] + [SupportedOSPlatform("browser")] + public static void EndInvokeJS(string argsJson) + { + WebAssemblyCallQueue.Schedule(argsJson, static argsJson => + { + // This is not expected to throw, as it takes care of converting any unhandled user code + // exceptions into a failure on the Task that was returned when calling InvokeAsync. + DotNetDispatcher.EndInvokeJS(Instance, argsJson); + }); + } + + [JSExport] + [SupportedOSPlatform("browser")] + public static void BeginInvokeDotNet(string? callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) + { + // Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID + // We only need one for any given call. This helps to work around the limitation that we can + // only pass a maximum of 4 args in a call from JS to Mono WebAssembly. + string? assemblyName; + long dotNetObjectId; + if (char.IsDigit(assemblyNameOrDotNetObjectId[0])) + { + dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId, CultureInfo.InvariantCulture); + assemblyName = null; + } + else + { + dotNetObjectId = default; + assemblyName = assemblyNameOrDotNetObjectId; + } + + var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId); + WebAssemblyCallQueue.Schedule((callInfo, argsJson), static state => + { + // This is not expected to throw, as it takes care of converting any unhandled user code + // exceptions into a failure on the JS Promise object. + DotNetDispatcher.BeginInvokeDotNet(Instance, state.callInfo, state.argsJson); + }); + } + + [SupportedOSPlatform("browser")] + [JSExport] + public static void UpdateRootComponentsCore(string operationsJson, string appState) + { + try + { + var operations = DeserializeOperations(operationsJson); + Instance.OnUpdateRootComponents?.Invoke(operations, appState); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error deserializing root component operations: {ex}"); + } + } + [DynamicDependency(JsonSerialized, typeof(RootComponentOperation))] [DynamicDependency(JsonSerialized, typeof(RootComponentOperationBatch))] [DynamicDependency(JsonSerialized, typeof(ComponentMarkerKey))] @@ -95,6 +150,13 @@ static WebRootComponentParameters DeserializeComponentParameters(ComponentMarker return new(parameters, definitions, values.AsReadOnly()); } + [JSExport] + [SupportedOSPlatform("browser")] + private static void ReceiveByteArrayFromJS(int id, byte[] data) + { + DotNetDispatcher.ReceiveByteArray(Instance, id, data); + } + /// protected override Task ReadJSDataAsStreamAsync(IJSStreamReference jsStreamReference, long totalLength, CancellationToken cancellationToken = default) => Task.FromResult(PullFromJSDataStream.CreateJSDataStream(this, jsStreamReference, totalLength, cancellationToken)); From 093e4c3ab728ea65749a4163e070575de43863fc Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 30 Mar 2026 13:41:57 +0200 Subject: [PATCH 2/2] more --- .../WebAssembly/test/EventDispatchTest.cs | 138 ------------------ 1 file changed, 138 deletions(-) delete mode 100644 src/Components/WebAssembly/WebAssembly/test/EventDispatchTest.cs diff --git a/src/Components/WebAssembly/WebAssembly/test/EventDispatchTest.cs b/src/Components/WebAssembly/WebAssembly/test/EventDispatchTest.cs deleted file mode 100644 index 4b7acdf9f1c0..000000000000 --- a/src/Components/WebAssembly/WebAssembly/test/EventDispatchTest.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.Web; - -namespace Microsoft.AspNetCore.Components.WebAssembly.Services; - -public class EventDispatchTest -{ - [Fact] - public void CreateFieldInfo_ReturnsNull_WhenFieldComponentIdIsNegative() - { - var result = DefaultWebAssemblyJSRuntime.CreateFieldInfo(-1, "someValue", false); - - Assert.Null(result); - } - - [Fact] - public void CreateFieldInfo_ReturnsNull_WhenFieldComponentIdIsMinusOne() - { - var result = DefaultWebAssemblyJSRuntime.CreateFieldInfo(-100, null, true); - - Assert.Null(result); - } - - [Fact] - public void CreateFieldInfo_SetsStringFieldValue_WhenFieldValueStringIsNotNull() - { - var result = DefaultWebAssemblyJSRuntime.CreateFieldInfo(42, "hello", true); - - Assert.NotNull(result); - Assert.Equal(42, result.ComponentId); - Assert.Equal("hello", result.FieldValue); - } - - [Fact] - public void CreateFieldInfo_SetsBoolFieldValue_WhenFieldValueStringIsNull_True() - { - var result = DefaultWebAssemblyJSRuntime.CreateFieldInfo(7, null, true); - - Assert.NotNull(result); - Assert.Equal(7, result.ComponentId); - Assert.Equal(true, result.FieldValue); - } - - [Fact] - public void CreateFieldInfo_SetsBoolFieldValue_WhenFieldValueStringIsNull_False() - { - var result = DefaultWebAssemblyJSRuntime.CreateFieldInfo(7, null, false); - - Assert.NotNull(result); - Assert.Equal(7, result.ComponentId); - Assert.Equal(false, result.FieldValue); - } - - [Fact] - public void CreateFieldInfo_UsesCachedBoxedBooleans() - { - var resultTrue1 = DefaultWebAssemblyJSRuntime.CreateFieldInfo(1, null, true); - var resultTrue2 = DefaultWebAssemblyJSRuntime.CreateFieldInfo(2, null, true); - var resultFalse1 = DefaultWebAssemblyJSRuntime.CreateFieldInfo(1, null, false); - var resultFalse2 = DefaultWebAssemblyJSRuntime.CreateFieldInfo(2, null, false); - - Assert.Same(resultTrue1!.FieldValue, resultTrue2!.FieldValue); - Assert.Same(resultFalse1!.FieldValue, resultFalse2!.FieldValue); - Assert.NotSame(resultTrue1.FieldValue, resultFalse1.FieldValue); - } - - [Fact] - public void CreateFieldInfo_SetsComponentId_WhenFieldComponentIdIsZero() - { - var result = DefaultWebAssemblyJSRuntime.CreateFieldInfo(0, null, false); - - Assert.NotNull(result); - Assert.Equal(0, result.ComponentId); - } - - [Fact] - public void UnflattenTouchPoints_ReturnsEmptyArray_WhenInputIsEmpty() - { - var result = DefaultWebAssemblyJSRuntime.UnflattenTouchPoints([]); - - Assert.Empty(result); - } - - [Fact] - public void UnflattenTouchPoints_ReturnsEmptyArray_WhenInputIsNull() - { - var result = DefaultWebAssemblyJSRuntime.UnflattenTouchPoints(null); - - Assert.Empty(result); - } - - [Fact] - public void UnflattenTouchPoints_ReturnsSingleTouchPoint() - { - double[] flat = [42, 100.5, 200.5, 300.5, 400.5, 500.5, 600.5]; - - var result = DefaultWebAssemblyJSRuntime.UnflattenTouchPoints(flat); - - Assert.Single(result); - Assert.Equal(42, result[0].Identifier); - Assert.Equal(100.5, result[0].ScreenX); - Assert.Equal(200.5, result[0].ScreenY); - Assert.Equal(300.5, result[0].ClientX); - Assert.Equal(400.5, result[0].ClientY); - Assert.Equal(500.5, result[0].PageX); - Assert.Equal(600.5, result[0].PageY); - } - - [Fact] - public void UnflattenTouchPoints_ReturnsMultipleTouchPoints() - { - double[] flat = - [ - 1, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, - 2, 11.0, 21.0, 31.0, 41.0, 51.0, 61.0, - 3, 12.0, 22.0, 32.0, 42.0, 52.0, 62.0, - ]; - - var result = DefaultWebAssemblyJSRuntime.UnflattenTouchPoints(flat); - - Assert.Equal(3, result.Length); - - Assert.Equal(1, result[0].Identifier); - Assert.Equal(10.0, result[0].ScreenX); - Assert.Equal(60.0, result[0].PageY); - - Assert.Equal(2, result[1].Identifier); - Assert.Equal(11.0, result[1].ScreenX); - Assert.Equal(41.0, result[1].ClientY); - - Assert.Equal(3, result[2].Identifier); - Assert.Equal(32.0, result[2].ClientX); - Assert.Equal(62.0, result[2].PageY); - } -}