Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/Components/Web.JS/src/Boot.WebAssembly.Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,25 @@ async function startCore(components: RootComponentManager<WebAssemblyComponentDe
}
};

function dispatchLocationChanged(uri: string, state: string | undefined, intercepted: boolean): Promise<void> {
return Blazor._internal.dotNetExports!.DispatchLocationChanged(uri, state ?? null, intercepted);
}
Blazor._internal.navigationManager.listenForNavigationEvents(WebRendererId.WebAssembly, async (uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
await dispatcher.invokeDotNetStaticMethodAsync(
'Microsoft.AspNetCore.Components.WebAssembly',
'NotifyLocationChanged',
uri,
state,
intercepted
);
}, async (callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
const shouldContinueNavigation = await dispatcher.invokeDotNetStaticMethodAsync<boolean>(
'Microsoft.AspNetCore.Components.WebAssembly',
'NotifyLocationChangingAsync',
uri,
state,
intercepted
);

async function dispatchLocationChanging(callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise<void> {
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.
Expand All @@ -148,7 +157,7 @@ async function startCore(components: RootComponentManager<WebAssemblyComponentDe
Blazor._internal.getInitialComponentsUpdate = () => 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) =>
Expand Down
21 changes: 1 addition & 20 deletions src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

// 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<void>;
DispatchLocationChanging: (uri: string, state: string | null, isInterceptedLink: boolean) => Promise<boolean>;
UpdateRootComponentsCore: (operationsJson: string, appState: string) => void;
}

// APIs invoked by hot reload
Expand Down
194 changes: 2 additions & 192 deletions src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, DotNet.DotNetObject>();
const rendererAttachedListeners: ((browserRendererId: number) => void)[] = [];
Expand Down Expand Up @@ -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<typeof Blazor._internal.dotNetExports>,
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<void> {
const interopMethods = getInteropMethods(browserRendererId);
interopMethods.invokeMethodAsync('UpdateRootComponents', operationsJson);
return interopMethods.invokeMethodAsync('UpdateRootComponents', operationsJson);
}

function getInteropMethods(rendererId: number): DotNet.DotNetObject {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,6 +39,8 @@ public sealed class WebAssemblyHostBuilder
/// </summary>
/// <param name="args">The argument passed to the application's main method.</param>
/// <returns>A <see cref="WebAssemblyHostBuilder"/>.</returns>
[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.
Expand Down
Loading
Loading