From 8e5e7d4ab461257a41f707c6fd486f605d09bc06 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Mon, 8 Feb 2021 12:51:39 +0000
Subject: [PATCH 01/25] Mark redundant extension methods as obsolete.
---
...EventCallbackFactoryEventArgsExtensions.cs | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs b/src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs
index d7f7d01ae205..e13959c43567 100644
--- a/src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs
+++ b/src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs
@@ -19,6 +19,7 @@ public static class WebEventCallbackFactoryEventArgsExtensions
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -37,6 +38,7 @@ public static EventCallback Create(this EventCallbackFactory
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -55,6 +57,7 @@ public static EventCallback Create(this EventCallbackFactory
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -73,6 +76,7 @@ public static EventCallback Create(this EventCallbackFactory fact
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -91,6 +95,7 @@ public static EventCallback Create(this EventCallbackFactory fact
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -109,6 +114,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -127,6 +133,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -145,6 +152,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -163,6 +171,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -181,6 +190,7 @@ public static EventCallback Create(this EventCallbackFactory
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -199,6 +209,7 @@ public static EventCallback Create(this EventCallbackFactory
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -217,6 +228,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -234,6 +246,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -252,6 +265,7 @@ public static EventCallback Create(this EventCallbackFactory f
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -270,6 +284,7 @@ public static EventCallback Create(this EventCallbackFactory f
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -288,6 +303,7 @@ public static EventCallback Create(this EventCallbackFactory
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -306,6 +322,7 @@ public static EventCallback Create(this EventCallbackFactory
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -324,6 +341,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
@@ -342,6 +360,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback)
{
if (factory == null)
@@ -360,6 +379,7 @@ public static EventCallback Create(this EventCallbackFactory fac
/// The event receiver.
/// The event callback.
/// The .
+ [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback)
{
if (factory == null)
From ea7c7c2b7b6880f2a8331529f70ba1ddb9e828eb Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Mon, 8 Feb 2021 13:39:22 +0000
Subject: [PATCH 02/25] Add E2E test showing polymorphic event handlers work
today
... because a subsequent implementation change would break this if it wasn't accounted for
---
.../test/E2ETest/Tests/EventTest.cs | 25 +++++++++++++
.../BasicTestApp/MouseEventComponent.razor | 35 ++++++++++++++-----
2 files changed, 51 insertions(+), 9 deletions(-)
diff --git a/src/Components/test/E2ETest/Tests/EventTest.cs b/src/Components/test/E2ETest/Tests/EventTest.cs
index e0d74eeffc04..69e9a558867f 100644
--- a/src/Components/test/E2ETest/Tests/EventTest.cs
+++ b/src/Components/test/E2ETest/Tests/EventTest.cs
@@ -300,6 +300,31 @@ public void RenderAttributesBeforeConnectedCallBack()
Browser.Contains(expectedContent, () => element.Text);
}
+ [Fact]
+ public void PolymorphicEventHandlersReceiveCorrectArgsSubclass()
+ {
+ // This is to show that the type of event argument received corresponds to the declared event
+ // name, and not to the argument type on the event handler delegate. Note that this is only
+ // supported (for back-compat) for the built-in standard web event types. For custom events,
+ // the eventargs deserialization type is determined purely by the delegate's parameters list.
+ Browser.MountTestComponent();
+
+ var elem = Browser.Exists(By.Id("polymorphic_event_elem"));
+
+ // Output is initially empty
+ var output = Browser.Exists(By.Id("output"));
+ Assert.Equal(string.Empty, output.Text);
+
+ // We can trigger a pointer event and receive a PointerEventArgs
+ new Actions(Browser).Click(elem).Perform();
+ Browser.Equal("Microsoft.AspNetCore.Components.Web.PointerEventArgs:mouse", () => output.Text);
+
+ // We can trigger a drag event and receive a DragEventArgs *on the same handler delegate*
+ Browser.FindElement(By.Id("clear_event_log")).Click();
+ new Actions(Browser).DragAndDrop(elem, Browser.FindElement(By.Id("other"))).Perform();
+ Browser.Equal("Microsoft.AspNetCore.Components.Web.DragEventArgs:1", () => output.Text);
+ }
+
void SendKeysSequentially(IWebElement target, string text)
{
// Calling it for each character works around some chars being skipped
diff --git a/src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor b/src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor
index 9b9d05eedcc3..d529d3094568 100644
--- a/src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor
@@ -23,12 +23,17 @@
Drop Target
-
+
Another input (to distract you)
+
+
+ Polymorphic args handler:
+
Click or drag me
+
@code {
@@ -39,56 +44,68 @@
{
DumpEvent(e);
message += "onmouseover,";
- StateHasChanged();
}
void OnMouseOut(MouseEventArgs e)
{
DumpEvent(e);
message += "onmouseout,";
- StateHasChanged();
}
void OnMouseMove(MouseEventArgs e)
{
DumpEvent(e);
message += "onmousemove,";
- StateHasChanged();
}
void OnMouseDown(MouseEventArgs e)
{
DumpEvent(e);
message += "onmousedown,";
- StateHasChanged();
}
void OnMouseUp(MouseEventArgs e)
{
DumpEvent(e);
message += "onmouseup,";
- StateHasChanged();
}
void OnPointerDown(PointerEventArgs e)
{
DumpEvent(e);
message += "onpointerdown";
- StateHasChanged();
}
void OnDragStart(DragEventArgs e)
{
DumpEvent(e);
message += "ondragstart,";
- StateHasChanged();
}
void OnDrop(DragEventArgs e)
{
DumpEvent(e);
message += "ondrop,";
- StateHasChanged();
+ }
+
+ void OnPolymorphicEvent(EventArgs e)
+ {
+ // The purpose of this handler is to show that, even though the declared args type is
+ // the EventArgs base class, at runtime we actually receive the subclass corresponding
+ // to the event that occurred. Note that this will only be supported for the built-in
+ // web event types (for back compatibility), and cannot work for any custom events,
+ // since we have no way to know which subclass you'd want for a custom event.
+ message += e.GetType().FullName;
+
+ switch (e)
+ {
+ case PointerEventArgs pointerEvent:
+ message += $":{pointerEvent.PointerType}";
+ break;
+ case DragEventArgs dragEvent:
+ message += $":{dragEvent.Buttons}";
+ break;
+ }
}
void DumpEvent(MouseEventArgs e)
From 7b582080c80ea1f547a4b7b66f1918dbb9bc34fc Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Mon, 8 Feb 2021 15:55:04 +0000
Subject: [PATCH 03/25] Clean up JS-side code to only send raw event args
(which does include the raw event type name), but nothing about args
deserialization type
---
.../Web.JS/src/Rendering/BrowserRenderer.ts | 10 +-
.../Web.JS/src/Rendering/EventDelegator.ts | 8 +-
.../Web.JS/src/Rendering/EventForDotNet.ts | 206 +++++++++---------
3 files changed, 109 insertions(+), 115 deletions(-)
diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
index 3fab6a2cd0c8..0980e9440689 100644
--- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
+++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
@@ -1,6 +1,6 @@
import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
import { EventDelegator } from './EventDelegator';
-import { EventForDotNet, UIEventArgs, EventArgsType } from './EventForDotNet';
+import { UIEventArgs } from './EventForDotNet';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
import { EventFieldInfo } from './EventFieldInfo';
@@ -462,7 +462,7 @@ export interface ComponentDescriptor {
export interface EventDescriptor {
browserRendererId: number;
eventHandlerId: number;
- eventArgsType: EventArgsType;
+ eventName: string;
eventFieldInfo: EventFieldInfo | null;
}
@@ -495,7 +495,7 @@ function raiseEvent(
event: Event,
browserRendererId: number,
eventHandlerId: number,
- eventArgs: EventForDotNet,
+ eventArgs: UIEventArgs,
eventFieldInfo: EventFieldInfo | null
): void {
if (preventDefaultEvents[event.type]) {
@@ -505,11 +505,11 @@ function raiseEvent(
const eventDescriptor = {
browserRendererId,
eventHandlerId,
- eventArgsType: eventArgs.type,
+ eventName: eventArgs.type,
eventFieldInfo: eventFieldInfo,
};
- dispatchEvent(eventDescriptor, eventArgs.data);
+ dispatchEvent(eventDescriptor, eventArgs);
}
function clearElement(element: Element) {
diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts
index 8f63d9406f2f..4595231b29fa 100644
--- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts
@@ -1,4 +1,4 @@
-import { EventForDotNet, UIEventArgs } from './EventForDotNet';
+import { fromDOMEvent, UIEventArgs } from './EventForDotNet';
import { EventFieldInfo } from './EventFieldInfo';
const nonBubblingEvents = toLookup([
@@ -25,7 +25,7 @@ const nonBubblingEvents = toLookup([
const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']);
export interface OnEventCallback {
- (event: Event, eventHandlerId: number, eventArgs: EventForDotNet, eventFieldInfo: EventFieldInfo | null): void;
+ (event: Event, eventHandlerId: number, eventArgs: UIEventArgs, eventFieldInfo: EventFieldInfo | null): void;
}
// Responsible for adding/removing the eventInfo on an expando property on DOM elements, and
@@ -107,7 +107,7 @@ export class EventDelegator {
// Scan up the element hierarchy, looking for any matching registered event handlers
let candidateElement = evt.target as Element | null;
- let eventArgs: EventForDotNet | null = null; // Populate lazily
+ let eventArgs: UIEventArgs | null = null; // Populate lazily
const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
let stopPropagationWasRequested = false;
while (candidateElement) {
@@ -117,7 +117,7 @@ export class EventDelegator {
if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) {
// We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgs) {
- eventArgs = EventForDotNet.fromDOMEvent(evt);
+ eventArgs = fromDOMEvent(evt);
}
const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
diff --git a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts b/src/Components/Web.JS/src/Rendering/EventForDotNet.ts
index 8c6222e775b5..7d4da189ad6b 100644
--- a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts
+++ b/src/Components/Web.JS/src/Rendering/EventForDotNet.ts
@@ -1,112 +1,108 @@
-export class EventForDotNet {
- public constructor(public readonly type: EventArgsType, public readonly data: TData) {
+export function fromDOMEvent(event: Event): UIEventArgs {
+ switch (event.type) {
+
+ case 'input':
+ case 'change':
+ return parseChangeEvent(event);
+
+ case 'copy':
+ case 'cut':
+ case 'paste':
+ return { type: event.type };
+
+ case 'drag':
+ case 'dragend':
+ case 'dragenter':
+ case 'dragleave':
+ case 'dragover':
+ case 'dragstart':
+ case 'drop':
+ return parseDragEvent(event);
+
+ case 'focus':
+ case 'blur':
+ case 'focusin':
+ case 'focusout':
+ return { type: event.type };
+
+ case 'keydown':
+ case 'keyup':
+ case 'keypress':
+ return parseKeyboardEvent(event as KeyboardEvent);
+
+ case 'contextmenu':
+ case 'click':
+ case 'mouseover':
+ case 'mouseout':
+ case 'mousemove':
+ case 'mousedown':
+ case 'mouseup':
+ case 'dblclick':
+ return parseMouseEvent(event as MouseEvent);
+
+ case 'error':
+ return parseErrorEvent(event as ErrorEvent);
+
+ case 'loadstart':
+ case 'timeout':
+ case 'abort':
+ case 'load':
+ case 'loadend':
+ case 'progress':
+ return parseProgressEvent(event as ProgressEvent);
+
+ case 'touchcancel':
+ case 'touchend':
+ case 'touchmove':
+ case 'touchenter':
+ case 'touchleave':
+ case 'touchstart':
+ return parseTouchEvent(event as TouchEvent);
+
+ case 'gotpointercapture':
+ case 'lostpointercapture':
+ case 'pointercancel':
+ case 'pointerdown':
+ case 'pointerenter':
+ case 'pointerleave':
+ case 'pointermove':
+ case 'pointerout':
+ case 'pointerover':
+ case 'pointerup':
+ return parsePointerEvent(event as PointerEvent);
+
+ case 'wheel':
+ case 'mousewheel':
+ return parseWheelEvent(event as WheelEvent);
+
+ case 'toggle':
+ return { type: event.type };
+
+ default:
+ return { type: event.type };
}
+}
- public static fromDOMEvent(event: Event): EventForDotNet {
- const element = event.target as Element;
- switch (event.type) {
-
- case 'input':
- case 'change': {
-
- if (isTimeBasedInput(element)) {
- const normalizedValue = normalizeTimeBasedValue(element);
- return new EventForDotNet('change', { type: event.type, value: normalizedValue });
- }
-
- const targetIsCheckbox = isCheckbox(element);
- const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
- return new EventForDotNet('change', { type: event.type, value: newValue });
- }
-
- case 'copy':
- case 'cut':
- case 'paste':
- return new EventForDotNet('clipboard', { type: event.type });
-
- case 'drag':
- case 'dragend':
- case 'dragenter':
- case 'dragleave':
- case 'dragover':
- case 'dragstart':
- case 'drop':
- return new EventForDotNet('drag', parseDragEvent(event));
-
- case 'focus':
- case 'blur':
- case 'focusin':
- case 'focusout':
- return new EventForDotNet('focus', { type: event.type });
-
- case 'keydown':
- case 'keyup':
- case 'keypress':
- return new EventForDotNet('keyboard', parseKeyboardEvent(event as KeyboardEvent));
-
- case 'contextmenu':
- case 'click':
- case 'mouseover':
- case 'mouseout':
- case 'mousemove':
- case 'mousedown':
- case 'mouseup':
- case 'dblclick':
- return new EventForDotNet('mouse', parseMouseEvent(event as MouseEvent));
-
- case 'error':
- return new EventForDotNet('error', parseErrorEvent(event as ErrorEvent));
-
- case 'loadstart':
- case 'timeout':
- case 'abort':
- case 'load':
- case 'loadend':
- case 'progress':
- return new EventForDotNet('progress', parseProgressEvent(event as ProgressEvent));
-
- case 'touchcancel':
- case 'touchend':
- case 'touchmove':
- case 'touchenter':
- case 'touchleave':
- case 'touchstart':
- return new EventForDotNet('touch', parseTouchEvent(event as TouchEvent));
-
- case 'gotpointercapture':
- case 'lostpointercapture':
- case 'pointercancel':
- case 'pointerdown':
- case 'pointerenter':
- case 'pointerleave':
- case 'pointermove':
- case 'pointerout':
- case 'pointerover':
- case 'pointerup':
- return new EventForDotNet('pointer', parsePointerEvent(event as PointerEvent));
-
- case 'wheel':
- case 'mousewheel':
- return new EventForDotNet('wheel', parseWheelEvent(event as WheelEvent));
-
- case 'toggle':
- return new EventForDotNet('toggle', { type: event.type });
-
- default:
- return new EventForDotNet('unknown', { type: event.type });
- }
+function parseChangeEvent(event: any): UIChangeEventArgs {
+ const element = event.target as Element;
+ if (isTimeBasedInput(element)) {
+ const normalizedValue = normalizeTimeBasedValue(element);
+ return { type: event.type, value: normalizedValue };
+ } else {
+ const targetIsCheckbox = isCheckbox(element);
+ const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
+ return { type: event.type, value: newValue };
}
}
-function parseDragEvent(event: any) {
+function parseDragEvent(event: any): UIDragEventArgs {
return {
...parseMouseEvent(event),
dataTransfer: event.dataTransfer,
-
};
}
-function parseWheelEvent(event: WheelEvent) {
+function parseWheelEvent(event: WheelEvent): UIWheelEventArgs {
return {
...parseMouseEvent(event),
deltaX: event.deltaX,
@@ -116,7 +112,7 @@ function parseWheelEvent(event: WheelEvent) {
};
}
-function parseErrorEvent(event: ErrorEvent) {
+function parseErrorEvent(event: ErrorEvent): UIErrorEventArgs {
return {
type: event.type,
message: event.message,
@@ -126,7 +122,7 @@ function parseErrorEvent(event: ErrorEvent) {
};
}
-function parseProgressEvent(event: ProgressEvent) {
+function parseProgressEvent(event: ProgressEvent): UIProgressEventArgs {
return {
type: event.type,
lengthComputable: event.lengthComputable,
@@ -135,7 +131,7 @@ function parseProgressEvent(event: ProgressEvent) {
};
}
-function parseTouchEvent(event: TouchEvent) {
+function parseTouchEvent(event: TouchEvent): UITouchEventArgs {
function parseTouch(touchList: TouchList) {
const touches: UITouchPoint[] = [];
@@ -168,7 +164,7 @@ function parseTouchEvent(event: TouchEvent) {
};
}
-function parseKeyboardEvent(event: KeyboardEvent) {
+function parseKeyboardEvent(event: KeyboardEvent): UIKeyboardEventArgs {
return {
type: event.type,
key: event.key,
@@ -182,7 +178,7 @@ function parseKeyboardEvent(event: KeyboardEvent) {
};
}
-function parsePointerEvent(event: PointerEvent) {
+function parsePointerEvent(event: PointerEvent): UIPointerEventArgs {
return {
...parseMouseEvent(event),
pointerId: event.pointerId,
@@ -196,7 +192,7 @@ function parsePointerEvent(event: PointerEvent) {
};
}
-function parseMouseEvent(event: MouseEvent) {
+function parseMouseEvent(event: MouseEvent): UIMouseEventArgs {
return {
type: event.type,
detail: event.detail,
@@ -251,8 +247,6 @@ function normalizeTimeBasedValue(element: HTMLInputElement): string {
// The following interfaces must be kept in sync with the UIEventArgs C# classes
-export type EventArgsType = 'change' | 'clipboard' | 'drag' | 'error' | 'focus' | 'keyboard' | 'mouse' | 'pointer' | 'progress' | 'touch' | 'unknown' | 'wheel' | 'toggle';
-
export interface UIEventArgs {
type: string;
}
From b37c11a80ff0b97d6ef7f2bb6a68f2aa1ed59b3d Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Mon, 8 Feb 2021 16:35:09 +0000
Subject: [PATCH 04/25] When necessary, parse the event args JSON using the
parameter type from the handler delegate
---
.../Components/src/PublicAPI.Unshipped.txt | 1 +
.../Components/src/RenderTree/Renderer.cs | 52 +++++-
.../Components/test/RendererTest.cs | 101 ++++++++++++
.../Server/src/Circuits/CircuitHost.cs | 2 +-
src/Components/Shared/src/WebEventData.cs | 155 +++++++++++++++---
.../Web/src/PublicAPI.Unshipped.txt | 4 +
src/Components/Web/src/WebEventDescriptor.cs | 2 +-
.../src/Infrastructure/JSInteropMethods.cs | 2 +-
.../ComponentHubInvalidEventTest.cs | 4 +-
.../InteropReliabilityTests.cs | 4 +-
10 files changed, 292 insertions(+), 35 deletions(-)
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 98ca3f39e896..2f6b57dcfd8a 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -15,6 +15,7 @@ Microsoft.AspNetCore.Components.DynamicComponent.Type.set -> void
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.CascadingTypeParameterAttribute(string! name) -> void
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.Name.get -> string!
+Microsoft.AspNetCore.Components.RenderTree.Renderer.GetEventArgsType(ulong eventHandlerId) -> System.Type!
static Microsoft.AspNetCore.Components.ParameterView.FromDictionary(System.Collections.Generic.IDictionary! parameters) -> Microsoft.AspNetCore.Components.ParameterView
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs) -> System.Threading.Tasks.Task!
*REMOVED*readonly Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit.RemovedAttributeName -> string
diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs
index bb6f2931c4e5..4b2232dc93d0 100644
--- a/src/Components/Components/src/RenderTree/Renderer.cs
+++ b/src/Components/Components/src/RenderTree/Renderer.cs
@@ -249,11 +249,7 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
{
Dispatcher.AssertAccess();
- if (!_eventBindings.TryGetValue(eventHandlerId, out var callback))
- {
- throw new ArgumentException($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", nameof(eventHandlerId));
- }
-
+ var callback = GetRequiredEventCallback(eventHandlerId);
Log.HandlingEvent(_logger, eventHandlerId, eventArgs);
if (fieldInfo != null)
@@ -291,6 +287,42 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
return result;
}
+ ///
+ /// Gets the event arguments type for the specified event handler.
+ ///
+ /// The value from the original event attribute.
+ /// The parameter type expected by the event handler. Normally this is a subclass of .
+ public Type GetEventArgsType(ulong eventHandlerId)
+ {
+ var callback = GetRequiredEventCallback(eventHandlerId);
+
+ // The DispatchEventAsync code paths allow for the case where Delegate or its method
+ // is null, and in this case the event receiver just receives null. This won't happen
+ // under normal circumstances, but to avoid creating a new failure scenario, allow for
+ // that edge case here too.
+ var parameterInfos = callback.Delegate?.Method.GetParameters();
+ if (parameterInfos == null || parameterInfos.Length == 0)
+ {
+ return typeof(EventArgs);
+ }
+ else if (parameterInfos.Length > 1)
+ {
+ throw new InvalidOperationException($"The event handler for event {eventHandlerId} declares more than one parameter. Only one is supported.");
+ }
+ else
+ {
+ var declaredType = parameterInfos[0].ParameterType;
+ if (typeof(EventArgs).IsAssignableFrom(declaredType))
+ {
+ return declaredType;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The event handler parameter type {declaredType.FullName} for event must inherit from {typeof(EventArgs).FullName}.");
+ }
+ }
+ }
+
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
{
if (frame.FrameTypeField != RenderTreeFrameType.Component)
@@ -404,6 +436,16 @@ internal void TrackReplacedEventHandlerId(ulong oldEventHandlerId, ulong newEven
_eventHandlerIdReplacements.Add(oldEventHandlerId, newEventHandlerId);
}
+ private EventCallback GetRequiredEventCallback(ulong eventHandlerId)
+ {
+ if (!_eventBindings.TryGetValue(eventHandlerId, out var callback))
+ {
+ throw new ArgumentException($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", nameof(eventHandlerId));
+ }
+
+ return callback;
+ }
+
private ulong FindLatestEventHandlerIdInChain(ulong eventHandlerId)
{
while (_eventHandlerIdReplacements.TryGetValue(eventHandlerId, out var replacementEventHandlerId))
diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs
index 5a5acc6e896d..3c12040f9a0d 100644
--- a/src/Components/Components/test/RendererTest.cs
+++ b/src/Components/Components/test/RendererTest.cs
@@ -484,6 +484,98 @@ public void CanDispatchEventsToTopLevelComponents()
Assert.Same(eventArgs, receivedArgs);
}
+ [Fact]
+ public void CanGetEventArgsTypeForHandler()
+ {
+ // Arrange: Render a component with an event handler
+ var renderer = new TestRenderer();
+
+ var component = new EventComponent
+ {
+ OnArbitraryDelegateEvent = (Func)(args => Task.CompletedTask),
+ };
+ var componentId = renderer.AssignRootComponentId(component);
+ component.TriggerRender();
+
+ var eventHandlerId = renderer.Batches.Single()
+ .ReferenceFrames
+ .First(frame => frame.AttributeValue != null)
+ .AttributeEventHandlerId;
+
+ // Assert: Can determine event args type
+ var eventArgsType = renderer.GetEventArgsType(eventHandlerId);
+ Assert.Same(typeof(DerivedEventArgs), eventArgsType);
+ }
+
+ [Fact]
+ public void CanGetEventArgsTypeForParameterlessHandler()
+ {
+ // Arrange: Render a component with an event handler
+ var renderer = new TestRenderer();
+
+ var component = new EventComponent
+ {
+ OnArbitraryDelegateEvent = (Func)(() => Task.CompletedTask),
+ };
+ var componentId = renderer.AssignRootComponentId(component);
+ component.TriggerRender();
+
+ var eventHandlerId = renderer.Batches.Single()
+ .ReferenceFrames
+ .First(frame => frame.AttributeValue != null)
+ .AttributeEventHandlerId;
+
+ // Assert: Can determine event args type
+ var eventArgsType = renderer.GetEventArgsType(eventHandlerId);
+ Assert.Same(typeof(EventArgs), eventArgsType);
+ }
+
+ [Fact]
+ public void CannotGetEventArgsTypeForMultiParameterHandler()
+ {
+ // Arrange: Render a component with an event handler
+ var renderer = new TestRenderer();
+
+ var component = new EventComponent
+ {
+ OnArbitraryDelegateEvent = (Action)((x, y) => { }),
+ };
+ var componentId = renderer.AssignRootComponentId(component);
+ component.TriggerRender();
+
+ var eventHandlerId = renderer.Batches.Single()
+ .ReferenceFrames
+ .First(frame => frame.AttributeValue != null)
+ .AttributeEventHandlerId;
+
+ // Assert: Cannot determine event args type
+ var ex = Assert.Throws(() => renderer.GetEventArgsType(eventHandlerId));
+ Assert.Contains("declares more than one parameter", ex.Message);
+ }
+
+ [Fact]
+ public void CannotGetEventArgsTypeForHandlerWithNonEventArgsParameter()
+ {
+ // Arrange: Render a component with an event handler
+ var renderer = new TestRenderer();
+
+ var component = new EventComponent
+ {
+ OnArbitraryDelegateEvent = (Action)(arg => { }),
+ };
+ var componentId = renderer.AssignRootComponentId(component);
+ component.TriggerRender();
+
+ var eventHandlerId = renderer.Batches.Single()
+ .ReferenceFrames
+ .First(frame => frame.AttributeValue != null)
+ .AttributeEventHandlerId;
+
+ // Assert: Cannot determine event args type
+ var ex = Assert.Throws(() => renderer.GetEventArgsType(eventHandlerId));
+ Assert.Contains($"must inherit from {typeof(EventArgs).FullName}", ex.Message);
+ }
+
[Fact]
public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers()
{
@@ -4224,6 +4316,9 @@ private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
[Parameter]
public EventCallback OnClickEventCallbackOfT { get; set; }
+ [Parameter]
+ public Delegate OnArbitraryDelegateEvent { get; set; }
+
public bool SkipElement { get; set; }
private int renderCount = 0;
@@ -4269,6 +4364,12 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddAttribute(5, "onclickaction", OnClickAsyncAction);
}
+
+ if (OnArbitraryDelegateEvent != null)
+ {
+ builder.AddAttribute(6, "onarbitrarydelegateevent", OnArbitraryDelegateEvent);
+ }
+
builder.CloseElement();
builder.CloseElement();
}
diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs
index 91222e9513cd..63c96073f682 100644
--- a/src/Components/Server/src/Circuits/CircuitHost.cs
+++ b/src/Components/Server/src/Circuits/CircuitHost.cs
@@ -399,7 +399,7 @@ public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson
WebEventData webEventData;
try
{
- webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson);
+ webEventData = WebEventData.Parse(Renderer, eventDescriptorJson, eventArgsJson);
}
catch (Exception ex)
{
diff --git a/src/Components/Shared/src/WebEventData.cs b/src/Components/Shared/src/WebEventData.cs
index 2940cc61d830..73830c548b4b 100644
--- a/src/Components/Shared/src/WebEventData.cs
+++ b/src/Components/Shared/src/WebEventData.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Microsoft.AspNetCore.Components.RenderTree;
@@ -10,8 +11,8 @@ namespace Microsoft.AspNetCore.Components.Web
internal class WebEventData
{
// This class represents the second half of parsing incoming event data,
- // once the type of the eventArgs becomes known.
- public static WebEventData Parse(string eventDescriptorJson, string eventArgsJson)
+ // once the event ID (and possibly the type of the eventArgs) becomes known.
+ public static WebEventData Parse(Renderer renderer, string eventDescriptorJson, string eventArgsJson)
{
WebEventDescriptor eventDescriptor;
try
@@ -24,17 +25,19 @@ public static WebEventData Parse(string eventDescriptorJson, string eventArgsJso
}
return Parse(
+ renderer,
eventDescriptor,
eventArgsJson);
}
- public static WebEventData Parse(WebEventDescriptor eventDescriptor, string eventArgsJson)
+ public static WebEventData Parse(Renderer renderer, WebEventDescriptor eventDescriptor, string eventArgsJson)
{
+ var parsedEventArgs = ParseEventArgsJson(renderer, eventDescriptor.EventHandlerId, eventDescriptor.EventName, eventArgsJson);
return new WebEventData(
eventDescriptor.BrowserRendererId,
eventDescriptor.EventHandlerId,
InterpretEventFieldInfo(eventDescriptor.EventFieldInfo),
- ParseEventArgsJson(eventDescriptor.EventHandlerId, eventDescriptor.EventArgsType, eventArgsJson));
+ parsedEventArgs);
}
private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo? eventFieldInfo, EventArgs eventArgs)
@@ -53,36 +56,140 @@ private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo
public EventArgs EventArgs { get; }
- private static EventArgs ParseEventArgsJson(ulong eventHandlerId, string eventArgsType, string eventArgsJson)
+ private static EventArgs ParseEventArgsJson(Renderer renderer, ulong eventHandlerId, string eventName, string eventArgsJson)
{
try
{
- return eventArgsType switch
+ if (TryGetStandardWebEventArgsType(eventName, out var eventArgsType))
{
- "change" => DeserializeChangeEventArgs(eventArgsJson),
- "clipboard" => Deserialize(eventArgsJson),
- "drag" => Deserialize(eventArgsJson),
- "error" => Deserialize(eventArgsJson),
- "focus" => Deserialize(eventArgsJson),
- "keyboard" => Deserialize(eventArgsJson),
- "mouse" => Deserialize(eventArgsJson),
- "pointer" => Deserialize(eventArgsJson),
- "progress" => Deserialize(eventArgsJson),
- "touch" => Deserialize(eventArgsJson),
- "unknown" => EventArgs.Empty,
- "wheel" => Deserialize(eventArgsJson),
- "toggle" => Deserialize(eventArgsJson),
- _ => throw new InvalidOperationException($"Unsupported event type '{eventArgsType}'. EventId: '{eventHandlerId}'."),
- };
+ // Special case for ChangeEventArgs because its value type can be one of
+ // several types, and System.Text.Json doesn't pick types dynamically
+ if (eventArgsType == typeof(ChangeEventArgs))
+ {
+ return DeserializeChangeEventArgs(eventArgsJson);
+ }
+ }
+ else
+ {
+ // For custom events, the args type is determined from the associated delegate
+ eventArgsType = renderer.GetEventArgsType(eventHandlerId);
+ }
+
+ return (EventArgs)JsonSerializer.Deserialize(eventArgsJson, eventArgsType, JsonSerializerOptionsProvider.Options)!;
}
catch (Exception e)
{
throw new InvalidOperationException($"There was an error parsing the event arguments. EventId: '{eventHandlerId}'.", e);
}
-
}
- static T Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializerOptionsProvider.Options)!;
+ private static bool TryGetStandardWebEventArgsType(string eventName, [MaybeNullWhen(false)] out Type type)
+ {
+ // For back-compatibility, we recognize the built-in list of web event names and hard-code
+ // rules about the deserialization type for their eventargs. This makes it possible to declare
+ // an event handler as receiving EventArgs, and have it actually receive a subclass at runtime
+ // depending on the event that was raised.
+ //
+ // The following list should remain in sync with EventForDotNet.ts.
+
+ switch (eventName)
+ {
+ case "input":
+ case "change":
+ type = typeof(ChangeEventArgs);
+ return true;
+
+ case "copy":
+ case "cut":
+ case "paste":
+ type = typeof(EventArgs);
+ return true;
+
+ case "drag":
+ case "dragend":
+ case "dragenter":
+ case "dragleave":
+ case "dragover":
+ case "dragstart":
+ case "drop":
+ type = typeof(DragEventArgs);
+ return true;
+
+ case "focus":
+ case "blur":
+ case "focusin":
+ case "focusout":
+ type = typeof(EventArgs);
+ return true;
+
+ case "keydown":
+ case "keyup":
+ case "keypress":
+ type = typeof(KeyboardEventArgs);
+ return true;
+
+ case "contextmenu":
+ case "click":
+ case "mouseover":
+ case "mouseout":
+ case "mousemove":
+ case "mousedown":
+ case "mouseup":
+ case "dblclick":
+ type = typeof(MouseEventArgs);
+ return true;
+
+ case "error":
+ type = typeof(ErrorEventArgs);
+ return true;
+
+ case "loadstart":
+ case "timeout":
+ case "abort":
+ case "load":
+ case "loadend":
+ case "progress":
+ type = typeof(ProgressEventArgs);
+ return true;
+
+ case "touchcancel":
+ case "touchend":
+ case "touchmove":
+ case "touchenter":
+ case "touchleave":
+ case "touchstart":
+ type = typeof(TouchEventArgs);
+ return true;
+
+ case "gotpointercapture":
+ case "lostpointercapture":
+ case "pointercancel":
+ case "pointerdown":
+ case "pointerenter":
+ case "pointerleave":
+ case "pointermove":
+ case "pointerout":
+ case "pointerover":
+ case "pointerup":
+ type = typeof(PointerEventArgs);
+ return true;
+
+ case "wheel":
+ case "mousewheel":
+ type = typeof(WheelEventArgs);
+ return true;
+
+ case "toggle":
+ type = typeof(EventArgs);
+ return true;
+
+ default:
+ // For custom event types, there are no built-in rules, so the deserialization type is
+ // determined by the parameter declared on the delegate.
+ type = null;
+ return false;
+ }
+ }
private static EventFieldInfo? InterpretEventFieldInfo(EventFieldInfo? fieldInfo)
{
@@ -111,6 +218,8 @@ private static EventArgs ParseEventArgsJson(ulong eventHandlerId, string eventAr
return null;
}
+ static T Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializerOptionsProvider.Options)!;
+
private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
{
var changeArgs = Deserialize(eventArgsJson);
diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt
index f887416cf935..f9125f300d01 100644
--- a/src/Components/Web/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Web/src/PublicAPI.Unshipped.txt
@@ -16,3 +16,7 @@ Microsoft.AspNetCore.Components.Forms.InputTextArea.Element.set -> void
*REMOVED*static Microsoft.AspNetCore.Components.Forms.BrowserFileExtensions.RequestImageFileAsync(this Microsoft.AspNetCore.Components.Forms.IBrowserFile! browserFile, string! format, int maxWith, int maxHeight) -> System.Threading.Tasks.ValueTask
static Microsoft.AspNetCore.Components.ElementReferenceExtensions.FocusAsync(this Microsoft.AspNetCore.Components.ElementReference elementReference, bool preventScroll) -> System.Threading.Tasks.ValueTask
static Microsoft.AspNetCore.Components.Forms.BrowserFileExtensions.RequestImageFileAsync(this Microsoft.AspNetCore.Components.Forms.IBrowserFile! browserFile, string! format, int maxWidth, int maxHeight) -> System.Threading.Tasks.ValueTask
+Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventName.get -> string!
+Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventName.set -> void
+*REMOVED*Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventArgsType.get -> string!
+*REMOVED*Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventArgsType.set -> void
diff --git a/src/Components/Web/src/WebEventDescriptor.cs b/src/Components/Web/src/WebEventDescriptor.cs
index 2e063b98f9e4..cc87d8f9d5fa 100644
--- a/src/Components/Web/src/WebEventDescriptor.cs
+++ b/src/Components/Web/src/WebEventDescriptor.cs
@@ -27,7 +27,7 @@ public sealed class WebEventDescriptor
///
/// For framework use only.
///
- public string EventArgsType { get; set; } = default!;
+ public string EventName { get; set; } = default!;
///
/// For framework use only.
diff --git a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs
index e31f92e8707a..c6eb32b09229 100644
--- a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs
+++ b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs
@@ -33,8 +33,8 @@ public static void NotifyLocationChanged(string uri, bool isInterceptedLink)
[JSInvokable(nameof(DispatchEvent))]
public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string eventArgsJson)
{
- var webEvent = WebEventData.Parse(eventDescriptor, eventArgsJson);
var renderer = RendererRegistry.Find(eventDescriptor.BrowserRendererId);
+ var webEvent = WebEventData.Parse(renderer, eventDescriptor, eventArgsJson);
return renderer.DispatchEventAsync(
webEvent.EventHandlerId,
webEvent.EventFieldInfo,
diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs
index 5d37553b5264..812259b3b4f8 100644
--- a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs
+++ b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs
@@ -44,7 +44,7 @@ public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings()
{
BrowserRendererId = 0,
EventHandlerId = 3,
- EventArgsType = "mouse",
+ EventName = "click",
});
// Act
@@ -71,7 +71,7 @@ public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings()
{
BrowserRendererId = 0,
EventHandlerId = 1990,
- EventArgsType = "mouse",
+ EventName = "click",
});
var eventArgs = new MouseEventArgs
diff --git a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs
index 7333672c2a18..87747364bda6 100644
--- a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs
+++ b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs
@@ -440,7 +440,7 @@ public async Task DispatchingEventsWithInvalidEventArgs()
{
BrowserRendererId = 0,
EventHandlerId = 6,
- EventArgsType = "mouse",
+ EventName = "click",
};
await Client.ExpectCircuitError(async () =>
@@ -478,7 +478,7 @@ public async Task DispatchingEventsWithInvalidEventHandlerId()
{
BrowserRendererId = 0,
EventHandlerId = 1,
- EventArgsType = "mouse",
+ EventName = "click",
};
await Client.ExpectCircuitError(async () =>
From eb391dfdd27277410033212441c583455935611b Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Mon, 8 Feb 2021 17:52:51 +0000
Subject: [PATCH 05/25] Fix event mapping
---
src/Components/Shared/src/WebEventData.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Components/Shared/src/WebEventData.cs b/src/Components/Shared/src/WebEventData.cs
index 73830c548b4b..77b514c18a31 100644
--- a/src/Components/Shared/src/WebEventData.cs
+++ b/src/Components/Shared/src/WebEventData.cs
@@ -102,7 +102,7 @@ private static bool TryGetStandardWebEventArgsType(string eventName, [MaybeNullW
case "copy":
case "cut":
case "paste":
- type = typeof(EventArgs);
+ type = typeof(ClipboardEventArgs);
return true;
case "drag":
@@ -119,7 +119,7 @@ private static bool TryGetStandardWebEventArgsType(string eventName, [MaybeNullW
case "blur":
case "focusin":
case "focusout":
- type = typeof(EventArgs);
+ type = typeof(FocusEventArgs);
return true;
case "keydown":
From 60d67eb7db1ee51c8c51cc51048e6a6ace63f73e Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 11:14:08 +0000
Subject: [PATCH 06/25] Simplify event dispatching on TypeScript side more.
Eliminate unnecessary types.
---
.../Web.JS/src/Rendering/BrowserRenderer.ts | 33 ++------
.../Web.JS/src/Rendering/EventDelegator.ts | 12 +--
.../Web.JS/src/Rendering/EventForDotNet.ts | 83 ++++++++-----------
.../src/Rendering/RendererEventDispatcher.ts | 8 +-
4 files changed, 50 insertions(+), 86 deletions(-)
diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
index 0980e9440689..9abee79b79d2 100644
--- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
+++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
@@ -1,6 +1,5 @@
import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
import { EventDelegator } from './EventDelegator';
-import { UIEventArgs } from './EventForDotNet';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
import { EventFieldInfo } from './EventFieldInfo';
@@ -20,12 +19,13 @@ export class BrowserRenderer {
private childComponentLocations: { [componentId: number]: LogicalElement } = {};
- private browserRendererId: number;
-
public constructor(browserRendererId: number) {
- this.browserRendererId = browserRendererId;
- this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => {
- raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs, eventFieldInfo);
+ this.eventDelegator = new EventDelegator((event, eventHandlerId, eventName, eventArgs, eventFieldInfo) => {
+ if (preventDefaultEvents[event.type]) {
+ event.preventDefault();
+ }
+
+ dispatchEvent({ browserRendererId, eventHandlerId, eventName, eventFieldInfo }, eventArgs);
});
// We don't yet know whether or not navigation interception will be enabled, but in case it will be,
@@ -491,27 +491,6 @@ function countDescendantFrames(batch: RenderBatch, frame: RenderTreeFrame): numb
}
}
-function raiseEvent(
- event: Event,
- browserRendererId: number,
- eventHandlerId: number,
- eventArgs: UIEventArgs,
- eventFieldInfo: EventFieldInfo | null
-): void {
- if (preventDefaultEvents[event.type]) {
- event.preventDefault();
- }
-
- const eventDescriptor = {
- browserRendererId,
- eventHandlerId,
- eventName: eventArgs.type,
- eventFieldInfo: eventFieldInfo,
- };
-
- dispatchEvent(eventDescriptor, eventArgs);
-}
-
function clearElement(element: Element) {
let childNode: Node | null;
while (childNode = element.firstChild) {
diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts
index 4595231b29fa..c0478e4d51ae 100644
--- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts
@@ -1,4 +1,4 @@
-import { fromDOMEvent, UIEventArgs } from './EventForDotNet';
+import { fromDOMEvent } from './EventForDotNet';
import { EventFieldInfo } from './EventFieldInfo';
const nonBubblingEvents = toLookup([
@@ -25,7 +25,7 @@ const nonBubblingEvents = toLookup([
const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']);
export interface OnEventCallback {
- (event: Event, eventHandlerId: number, eventArgs: UIEventArgs, eventFieldInfo: EventFieldInfo | null): void;
+ (event: Event, eventHandlerId: number, eventName: string, eventArgs: any, eventFieldInfo: EventFieldInfo | null): void;
}
// Responsible for adding/removing the eventInfo on an expando property on DOM elements, and
@@ -107,7 +107,8 @@ export class EventDelegator {
// Scan up the element hierarchy, looking for any matching registered event handlers
let candidateElement = evt.target as Element | null;
- let eventArgs: UIEventArgs | null = null; // Populate lazily
+ let eventArgs: any = null; // Populate lazily
+ let eventArgsIsPopulated = false;
const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
let stopPropagationWasRequested = false;
while (candidateElement) {
@@ -116,12 +117,13 @@ export class EventDelegator {
const handlerInfo = handlerInfos.getHandler(evt.type);
if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) {
// We are going to raise an event for this element, so prepare info needed by the .NET code
- if (!eventArgs) {
+ if (!eventArgsIsPopulated) {
eventArgs = fromDOMEvent(evt);
+ eventArgsIsPopulated = true;
}
const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
- this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs, eventFieldInfo);
+ this.onEvent(evt, handlerInfo.eventHandlerId, evt.type, eventArgs, eventFieldInfo);
}
if (handlerInfos.stopPropagation(evt.type)) {
diff --git a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts b/src/Components/Web.JS/src/Rendering/EventForDotNet.ts
index 7d4da189ad6b..da8fb6c83f87 100644
--- a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts
+++ b/src/Components/Web.JS/src/Rendering/EventForDotNet.ts
@@ -1,4 +1,4 @@
-export function fromDOMEvent(event: Event): UIEventArgs {
+export function fromDOMEvent(event: Event): any {
switch (event.type) {
case 'input':
@@ -8,7 +8,7 @@ export function fromDOMEvent(event: Event): UIEventArgs {
case 'copy':
case 'cut':
case 'paste':
- return { type: event.type };
+ return {};
case 'drag':
case 'dragend':
@@ -23,7 +23,7 @@ export function fromDOMEvent(event: Event): UIEventArgs {
case 'blur':
case 'focusin':
case 'focusout':
- return { type: event.type };
+ return {};
case 'keydown':
case 'keyup':
@@ -76,33 +76,33 @@ export function fromDOMEvent(event: Event): UIEventArgs {
return parseWheelEvent(event as WheelEvent);
case 'toggle':
- return { type: event.type };
+ return {};
default:
- return { type: event.type };
+ return {};
}
}
-function parseChangeEvent(event: any): UIChangeEventArgs {
+function parseChangeEvent(event: any): ChangeEventArgs {
const element = event.target as Element;
if (isTimeBasedInput(element)) {
const normalizedValue = normalizeTimeBasedValue(element);
- return { type: event.type, value: normalizedValue };
+ return { value: normalizedValue };
} else {
const targetIsCheckbox = isCheckbox(element);
const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
- return { type: event.type, value: newValue };
+ return { value: newValue };
}
}
-function parseDragEvent(event: any): UIDragEventArgs {
+function parseDragEvent(event: any): DragEventArgs {
return {
...parseMouseEvent(event),
dataTransfer: event.dataTransfer,
};
}
-function parseWheelEvent(event: WheelEvent): UIWheelEventArgs {
+function parseWheelEvent(event: WheelEvent): WheelEventArgs {
return {
...parseMouseEvent(event),
deltaX: event.deltaX,
@@ -112,9 +112,8 @@ function parseWheelEvent(event: WheelEvent): UIWheelEventArgs {
};
}
-function parseErrorEvent(event: ErrorEvent): UIErrorEventArgs {
+function parseErrorEvent(event: ErrorEvent): ErrorEventArgs {
return {
- type: event.type,
message: event.message,
filename: event.filename,
lineno: event.lineno,
@@ -122,19 +121,18 @@ function parseErrorEvent(event: ErrorEvent): UIErrorEventArgs {
};
}
-function parseProgressEvent(event: ProgressEvent): UIProgressEventArgs {
+function parseProgressEvent(event: ProgressEvent): ProgressEventArgs {
return {
- type: event.type,
lengthComputable: event.lengthComputable,
loaded: event.loaded,
total: event.total,
};
}
-function parseTouchEvent(event: TouchEvent): UITouchEventArgs {
+function parseTouchEvent(event: TouchEvent): TouchEventArgs {
function parseTouch(touchList: TouchList) {
- const touches: UITouchPoint[] = [];
+ const touches: TouchPoint[] = [];
for (let i = 0; i < touchList.length; i++) {
const touch = touchList[i];
@@ -152,7 +150,6 @@ function parseTouchEvent(event: TouchEvent): UITouchEventArgs {
}
return {
- type: event.type,
detail: event.detail,
touches: parseTouch(event.touches),
targetTouches: parseTouch(event.targetTouches),
@@ -164,9 +161,8 @@ function parseTouchEvent(event: TouchEvent): UITouchEventArgs {
};
}
-function parseKeyboardEvent(event: KeyboardEvent): UIKeyboardEventArgs {
+function parseKeyboardEvent(event: KeyboardEvent): KeyboardEventArgs {
return {
- type: event.type,
key: event.key,
code: event.code,
location: event.location,
@@ -178,7 +174,7 @@ function parseKeyboardEvent(event: KeyboardEvent): UIKeyboardEventArgs {
};
}
-function parsePointerEvent(event: PointerEvent): UIPointerEventArgs {
+function parsePointerEvent(event: PointerEvent): PointerEventArgs {
return {
...parseMouseEvent(event),
pointerId: event.pointerId,
@@ -192,9 +188,8 @@ function parsePointerEvent(event: PointerEvent): UIPointerEventArgs {
};
}
-function parseMouseEvent(event: MouseEvent): UIMouseEventArgs {
+function parseMouseEvent(event: MouseEvent): MouseEventArgs {
return {
- type: event.type,
detail: event.detail,
screenX: event.screenX,
screenY: event.screenY,
@@ -247,20 +242,13 @@ function normalizeTimeBasedValue(element: HTMLInputElement): string {
// The following interfaces must be kept in sync with the UIEventArgs C# classes
-export interface UIEventArgs {
- type: string;
-}
-
-interface UIChangeEventArgs extends UIEventArgs {
+interface ChangeEventArgs {
value: string | boolean;
}
-interface UIClipboardEventArgs extends UIEventArgs {
-}
-
-interface UIDragEventArgs extends UIEventArgs {
+interface DragEventArgs {
detail: number;
- dataTransfer: UIDataTransfer;
+ dataTransfer: DataTransfer;
screenX: number;
screenY: number;
clientX: number;
@@ -273,20 +261,20 @@ interface UIDragEventArgs extends UIEventArgs {
metaKey: boolean;
}
-interface UIDataTransfer {
+interface DataTransfer {
dropEffect: string;
effectAllowed: string;
files: string[];
- items: UIDataTransferItem[];
+ items: DataTransferItem[];
types: string[];
}
-interface UIDataTransferItem {
+interface DataTransferItem {
kind: string;
type: string;
}
-interface UIErrorEventArgs extends UIEventArgs {
+interface ErrorEventArgs {
message: string;
filename: string;
lineno: number;
@@ -296,10 +284,7 @@ interface UIErrorEventArgs extends UIEventArgs {
// do that. https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
}
-interface UIFocusEventArgs extends UIEventArgs {
-}
-
-interface UIKeyboardEventArgs extends UIEventArgs {
+interface KeyboardEventArgs {
key: string;
code: string;
location: number;
@@ -310,7 +295,7 @@ interface UIKeyboardEventArgs extends UIEventArgs {
metaKey: boolean;
}
-interface UIMouseEventArgs extends UIEventArgs {
+interface MouseEventArgs {
detail: number;
screenX: number;
screenY: number;
@@ -326,7 +311,7 @@ interface UIMouseEventArgs extends UIEventArgs {
metaKey: boolean;
}
-interface UIPointerEventArgs extends UIMouseEventArgs {
+interface PointerEventArgs extends MouseEventArgs {
pointerId: number;
width: number;
height: number;
@@ -337,24 +322,24 @@ interface UIPointerEventArgs extends UIMouseEventArgs {
isPrimary: boolean;
}
-interface UIProgressEventArgs extends UIEventArgs {
+interface ProgressEventArgs {
lengthComputable: boolean;
loaded: number;
total: number;
}
-interface UITouchEventArgs extends UIEventArgs {
+interface TouchEventArgs {
detail: number;
- touches: UITouchPoint[];
- targetTouches: UITouchPoint[];
- changedTouches: UITouchPoint[];
+ touches: TouchPoint[];
+ targetTouches: TouchPoint[];
+ changedTouches: TouchPoint[];
ctrlKey: boolean;
shiftKey: boolean;
altKey: boolean;
metaKey: boolean;
}
-interface UITouchPoint {
+interface TouchPoint {
identifier: number;
screenX: number;
screenY: number;
@@ -364,7 +349,7 @@ interface UITouchPoint {
pageY: number;
}
-interface UIWheelEventArgs extends UIMouseEventArgs {
+interface WheelEventArgs extends MouseEventArgs {
deltaX: number;
deltaY: number;
deltaZ: number;
diff --git a/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts b/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts
index 1b4870dac2bb..a18d9e17e8d4 100644
--- a/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts
+++ b/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts
@@ -1,11 +1,9 @@
import { EventDescriptor } from './BrowserRenderer';
-import { UIEventArgs } from './EventForDotNet';
-
-type EventDispatcher = (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => void;
+type EventDispatcher = (eventDescriptor: EventDescriptor, eventArgs: any) => void;
let eventDispatcherInstance: EventDispatcher;
-export function dispatchEvent(eventDescriptor: EventDescriptor, eventArgs: UIEventArgs): void {
+export function dispatchEvent(eventDescriptor: EventDescriptor, eventArgs: any): void {
if (!eventDispatcherInstance) {
throw new Error('eventDispatcher not initialized. Call \'setEventDispatcher\' to configure it.');
}
@@ -13,6 +11,6 @@ export function dispatchEvent(eventDescriptor: EventDescriptor, eventArgs: UIEve
eventDispatcherInstance(eventDescriptor, eventArgs);
}
-export function setEventDispatcher(newDispatcher: (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => void): void {
+export function setEventDispatcher(newDispatcher: (eventDescriptor: EventDescriptor, eventArgs: any) => void): void {
eventDispatcherInstance = newDispatcher;
}
From f0a78bbdeb8e2043219b97c5ff393e17e68a4934 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 11:16:12 +0000
Subject: [PATCH 07/25] Rename file for clarity
---
.../src/Rendering/{EventForDotNet.ts => EventArgsFactory.ts} | 2 +-
src/Components/Web.JS/src/Rendering/EventDelegator.ts | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
rename src/Components/Web.JS/src/Rendering/{EventForDotNet.ts => EventArgsFactory.ts} (99%)
diff --git a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts b/src/Components/Web.JS/src/Rendering/EventArgsFactory.ts
similarity index 99%
rename from src/Components/Web.JS/src/Rendering/EventForDotNet.ts
rename to src/Components/Web.JS/src/Rendering/EventArgsFactory.ts
index da8fb6c83f87..1f690a4e8ba9 100644
--- a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts
+++ b/src/Components/Web.JS/src/Rendering/EventArgsFactory.ts
@@ -1,4 +1,4 @@
-export function fromDOMEvent(event: Event): any {
+export function createEventArgsFromDOMEvent(event: Event): any {
switch (event.type) {
case 'input':
diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts
index c0478e4d51ae..8b32b38049e3 100644
--- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts
@@ -1,4 +1,4 @@
-import { fromDOMEvent } from './EventForDotNet';
+import { createEventArgsFromDOMEvent } from './EventArgsFactory';
import { EventFieldInfo } from './EventFieldInfo';
const nonBubblingEvents = toLookup([
@@ -118,7 +118,7 @@ export class EventDelegator {
if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) {
// We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgsIsPopulated) {
- eventArgs = fromDOMEvent(evt);
+ eventArgs = createEventArgsFromDOMEvent(evt);
eventArgsIsPopulated = true;
}
From 6d2694d28623f5ca9510e7eee29ece93c2a7a4a1 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 11:21:42 +0000
Subject: [PATCH 08/25] Migrate "preventDefault for submit" behavior into
EventDelegator because that's where other similar responsibilities are
---
.../Web.JS/src/Rendering/BrowserRenderer.ts | 7 +------
.../Web.JS/src/Rendering/EventDelegator.ts | 12 ++++++++++--
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
index 9abee79b79d2..41c5c1eeeef5 100644
--- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
+++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
@@ -8,7 +8,6 @@ import { attachToEventDelegator as attachNavigationManagerToEventDelegator } fro
const selectValuePropname = '_blazorSelectValue';
const sharedTemplateElemForParsing = document.createElement('template');
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
-const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {};
const internalAttributeNamePrefix = '__internal_';
const eventPreventDefaultAttributeNamePrefix = 'preventDefault_';
@@ -20,11 +19,7 @@ export class BrowserRenderer {
private childComponentLocations: { [componentId: number]: LogicalElement } = {};
public constructor(browserRendererId: number) {
- this.eventDelegator = new EventDelegator((event, eventHandlerId, eventName, eventArgs, eventFieldInfo) => {
- if (preventDefaultEvents[event.type]) {
- event.preventDefault();
- }
-
+ this.eventDelegator = new EventDelegator((eventHandlerId, eventName, eventArgs, eventFieldInfo) => {
dispatchEvent({ browserRendererId, eventHandlerId, eventName, eventFieldInfo }, eventArgs);
});
diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts
index 8b32b38049e3..2a1f3d6dea6d 100644
--- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts
@@ -22,10 +22,12 @@ const nonBubblingEvents = toLookup([
'DOMNodeRemovedFromDocument',
]);
+const alwaysPreventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
+
const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']);
export interface OnEventCallback {
- (event: Event, eventHandlerId: number, eventName: string, eventArgs: any, eventFieldInfo: EventFieldInfo | null): void;
+ (eventHandlerId: number, eventName: string, eventArgs: any, eventFieldInfo: EventFieldInfo | null): void;
}
// Responsible for adding/removing the eventInfo on an expando property on DOM elements, and
@@ -122,8 +124,14 @@ export class EventDelegator {
eventArgsIsPopulated = true;
}
+ // For certain built-in events, having any .NET handler implicitly means we will prevent
+ // the browser's default behavior
+ if (alwaysPreventDefaultEvents.hasOwnProperty(evt.type)) {
+ evt.preventDefault();
+ }
+
const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
- this.onEvent(evt, handlerInfo.eventHandlerId, evt.type, eventArgs, eventFieldInfo);
+ this.onEvent(handlerInfo.eventHandlerId, evt.type, eventArgs, eventFieldInfo);
}
if (handlerInfos.stopPropagation(evt.type)) {
From 1792351788042d12774c5f7122bd96be5124b9c9 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 11:28:05 +0000
Subject: [PATCH 09/25] Put event-centric files into an Events directory for
more clarity
---
src/Components/Web.JS/src/Boot.Server.ts | 2 +-
src/Components/Web.JS/src/Boot.WebAssembly.ts | 2 +-
.../Web.JS/src/Rendering/BrowserRenderer.ts | 12 ++----------
.../src/Rendering/{ => Events}/EventArgsFactory.ts | 0
.../src/Rendering/{ => Events}/EventDelegator.ts | 0
.../EventDispatcher.ts} | 10 +++++++++-
.../src/Rendering/{ => Events}/EventFieldInfo.ts | 0
.../Web.JS/src/Services/NavigationManager.ts | 2 +-
8 files changed, 14 insertions(+), 14 deletions(-)
rename src/Components/Web.JS/src/Rendering/{ => Events}/EventArgsFactory.ts (100%)
rename src/Components/Web.JS/src/Rendering/{ => Events}/EventDelegator.ts (100%)
rename src/Components/Web.JS/src/Rendering/{RendererEventDispatcher.ts => Events/EventDispatcher.ts} (73%)
rename src/Components/Web.JS/src/Rendering/{ => Events}/EventFieldInfo.ts (100%)
diff --git a/src/Components/Web.JS/src/Boot.Server.ts b/src/Components/Web.JS/src/Boot.Server.ts
index ca6ddf28606d..8fd6135e0be7 100644
--- a/src/Components/Web.JS/src/Boot.Server.ts
+++ b/src/Components/Web.JS/src/Boot.Server.ts
@@ -8,7 +8,7 @@ import { RenderQueue } from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers';
import { LogLevel, Logger } from './Platform/Logging/Logger';
import { CircuitDescriptor } from './Platform/Circuits/CircuitManager';
-import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
+import { setEventDispatcher } from './Rendering/Events/EventDispatcher';
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts
index d5acaae49b4a..235a5b9ecd10 100644
--- a/src/Components/Web.JS/src/Boot.WebAssembly.ts
+++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts
@@ -5,7 +5,7 @@ import { monoPlatform } from './Platform/Mono/MonoPlatform';
import { renderBatch, getRendererer, attachRootComponentToElement, attachRootComponentToLogicalElement } from './Rendering/Renderer';
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
import { shouldAutoStart } from './BootCommon';
-import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
+import { setEventDispatcher } from './Rendering/Events/EventDispatcher';
import { WebAssemblyResourceLoader } from './Platform/WebAssemblyResourceLoader';
import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
import { BootConfigResult } from './Platform/BootConfig';
diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
index 41c5c1eeeef5..5db19618b7e5 100644
--- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
+++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
@@ -1,9 +1,8 @@
import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
-import { EventDelegator } from './EventDelegator';
+import { EventDelegator } from './Events/EventDelegator';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
-import { EventFieldInfo } from './EventFieldInfo';
-import { dispatchEvent } from './RendererEventDispatcher';
+import { dispatchEvent } from './Events/EventDispatcher';
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
const selectValuePropname = '_blazorSelectValue';
const sharedTemplateElemForParsing = document.createElement('template');
@@ -454,13 +453,6 @@ export interface ComponentDescriptor {
end: Node;
}
-export interface EventDescriptor {
- browserRendererId: number;
- eventHandlerId: number;
- eventName: string;
- eventFieldInfo: EventFieldInfo | null;
-}
-
function parseMarkup(markup: string, isSvg: boolean) {
if (isSvg) {
sharedSvgElemForParsing.innerHTML = markup || ' ';
diff --git a/src/Components/Web.JS/src/Rendering/EventArgsFactory.ts b/src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts
similarity index 100%
rename from src/Components/Web.JS/src/Rendering/EventArgsFactory.ts
rename to src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts
diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
similarity index 100%
rename from src/Components/Web.JS/src/Rendering/EventDelegator.ts
rename to src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
diff --git a/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts b/src/Components/Web.JS/src/Rendering/Events/EventDispatcher.ts
similarity index 73%
rename from src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts
rename to src/Components/Web.JS/src/Rendering/Events/EventDispatcher.ts
index a18d9e17e8d4..244052eabbf3 100644
--- a/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventDispatcher.ts
@@ -1,4 +1,12 @@
-import { EventDescriptor } from './BrowserRenderer';
+import { EventFieldInfo } from './EventFieldInfo';
+
+export interface EventDescriptor {
+ browserRendererId: number;
+ eventHandlerId: number;
+ eventName: string;
+ eventFieldInfo: EventFieldInfo | null;
+}
+
type EventDispatcher = (eventDescriptor: EventDescriptor, eventArgs: any) => void;
let eventDispatcherInstance: EventDispatcher;
diff --git a/src/Components/Web.JS/src/Rendering/EventFieldInfo.ts b/src/Components/Web.JS/src/Rendering/Events/EventFieldInfo.ts
similarity index 100%
rename from src/Components/Web.JS/src/Rendering/EventFieldInfo.ts
rename to src/Components/Web.JS/src/Rendering/Events/EventFieldInfo.ts
diff --git a/src/Components/Web.JS/src/Services/NavigationManager.ts b/src/Components/Web.JS/src/Services/NavigationManager.ts
index 091481be13e3..66e4aad32c90 100644
--- a/src/Components/Web.JS/src/Services/NavigationManager.ts
+++ b/src/Components/Web.JS/src/Services/NavigationManager.ts
@@ -1,6 +1,6 @@
import '@microsoft/dotnet-js-interop';
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
-import { EventDelegator } from '../Rendering/EventDelegator';
+import { EventDelegator } from '../Rendering/Events/EventDelegator';
let hasEnabledNavigationInterception = false;
let hasRegisteredNavigationEventListeners = false;
From badc38f41c25e3036bcc6e488f506a1f6a6550c6 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 11:33:24 +0000
Subject: [PATCH 10/25] Disentangle event dispatch from BrowserRenderer
---
.../Web.JS/src/Rendering/BrowserRenderer.ts | 5 +----
.../Web.JS/src/Rendering/Events/EventDelegator.ts | 15 ++++++++-------
2 files changed, 9 insertions(+), 11 deletions(-)
diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
index 5db19618b7e5..d541724dc547 100644
--- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
+++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
@@ -2,7 +2,6 @@ import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, Edit
import { EventDelegator } from './Events/EventDelegator';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
-import { dispatchEvent } from './Events/EventDispatcher';
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
const selectValuePropname = '_blazorSelectValue';
const sharedTemplateElemForParsing = document.createElement('template');
@@ -18,9 +17,7 @@ export class BrowserRenderer {
private childComponentLocations: { [componentId: number]: LogicalElement } = {};
public constructor(browserRendererId: number) {
- this.eventDelegator = new EventDelegator((eventHandlerId, eventName, eventArgs, eventFieldInfo) => {
- dispatchEvent({ browserRendererId, eventHandlerId, eventName, eventFieldInfo }, eventArgs);
- });
+ this.eventDelegator = new EventDelegator(browserRendererId);
// We don't yet know whether or not navigation interception will be enabled, but in case it will be,
// we wire up the navigation manager to the event delegator so it has the option to participate
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
index 2a1f3d6dea6d..403d2d667f8e 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
@@ -1,5 +1,6 @@
import { createEventArgsFromDOMEvent } from './EventArgsFactory';
import { EventFieldInfo } from './EventFieldInfo';
+import { dispatchEvent } from './EventDispatcher';
const nonBubblingEvents = toLookup([
'abort',
@@ -26,10 +27,6 @@ const alwaysPreventDefaultEvents: { [eventType: string]: boolean } = { submit: t
const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']);
-export interface OnEventCallback {
- (eventHandlerId: number, eventName: string, eventArgs: any, eventFieldInfo: EventFieldInfo | null): void;
-}
-
// Responsible for adding/removing the eventInfo on an expando property on DOM elements, and
// calling an EventInfoStore that deals with registering/unregistering the underlying delegated
// event listeners as required (and also maps actual events back to the given callback).
@@ -42,7 +39,7 @@ export class EventDelegator {
private eventInfoStore: EventInfoStore;
- constructor(private onEvent: OnEventCallback) {
+ constructor(private browserRendererId: number) {
const eventDelegatorId = ++EventDelegator.nextEventDelegatorId;
this.eventsCollectionKey = `_blazorEvents_${eventDelegatorId}`;
this.eventInfoStore = new EventInfoStore(this.onGlobalEvent.bind(this));
@@ -130,8 +127,12 @@ export class EventDelegator {
evt.preventDefault();
}
- const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
- this.onEvent(handlerInfo.eventHandlerId, evt.type, eventArgs, eventFieldInfo);
+ dispatchEvent({
+ browserRendererId: this.browserRendererId,
+ eventHandlerId: handlerInfo.eventHandlerId,
+ eventName: evt.type,
+ eventFieldInfo: EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt)
+ }, eventArgs);
}
if (handlerInfos.stopPropagation(evt.type)) {
From c832baad08ca5e321deb04852c34d47370cfc2b1 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 11:55:26 +0000
Subject: [PATCH 11/25] Update comment
---
src/Components/Shared/src/WebEventData.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Components/Shared/src/WebEventData.cs b/src/Components/Shared/src/WebEventData.cs
index 77b514c18a31..ca3cc6ac4332 100644
--- a/src/Components/Shared/src/WebEventData.cs
+++ b/src/Components/Shared/src/WebEventData.cs
@@ -90,7 +90,7 @@ private static bool TryGetStandardWebEventArgsType(string eventName, [MaybeNullW
// an event handler as receiving EventArgs, and have it actually receive a subclass at runtime
// depending on the event that was raised.
//
- // The following list should remain in sync with EventForDotNet.ts.
+ // The following list should remain in sync with EventArgsFactory.ts.
switch (eventName)
{
From 08967e813ca5104d4aa7de17a56a2e12cf5d54c3 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 13:00:14 +0000
Subject: [PATCH 12/25] Add a cache for the handler->argstype lookup
---
.../src/RenderTree/EventArgsTypeCache.cs | 42 +++++++++++++++++++
.../Components/src/RenderTree/Renderer.cs | 26 ++----------
2 files changed, 46 insertions(+), 22 deletions(-)
create mode 100644 src/Components/Components/src/RenderTree/EventArgsTypeCache.cs
diff --git a/src/Components/Components/src/RenderTree/EventArgsTypeCache.cs b/src/Components/Components/src/RenderTree/EventArgsTypeCache.cs
new file mode 100644
index 000000000000..314545941589
--- /dev/null
+++ b/src/Components/Components/src/RenderTree/EventArgsTypeCache.cs
@@ -0,0 +1,42 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Reflection;
+
+namespace Microsoft.AspNetCore.Components.RenderTree
+{
+ internal static class EventArgsTypeCache
+ {
+ private static ConcurrentDictionary Cache = new ConcurrentDictionary();
+
+ public static Type GetEventArgsType(MethodInfo methodInfo)
+ {
+ return Cache.GetOrAdd(methodInfo, methodInfo =>
+ {
+ var parameterInfos = methodInfo.GetParameters();
+ if (parameterInfos.Length == 0)
+ {
+ return typeof(EventArgs);
+ }
+ else if (parameterInfos.Length > 1)
+ {
+ throw new InvalidOperationException($"The method {methodInfo} cannot be used as an event handler because it declares more than one parameter.");
+ }
+ else
+ {
+ var declaredType = parameterInfos[0].ParameterType;
+ if (typeof(EventArgs).IsAssignableFrom(declaredType))
+ {
+ return declaredType;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The event handler parameter type {declaredType.FullName} for event must inherit from {typeof(EventArgs).FullName}.");
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs
index 4b2232dc93d0..8e058f1da9a3 100644
--- a/src/Components/Components/src/RenderTree/Renderer.cs
+++ b/src/Components/Components/src/RenderTree/Renderer.cs
@@ -294,33 +294,15 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
/// The parameter type expected by the event handler. Normally this is a subclass of .
public Type GetEventArgsType(ulong eventHandlerId)
{
- var callback = GetRequiredEventCallback(eventHandlerId);
+ var methodInfo = GetRequiredEventCallback(eventHandlerId).Delegate?.Method;
// The DispatchEventAsync code paths allow for the case where Delegate or its method
// is null, and in this case the event receiver just receives null. This won't happen
// under normal circumstances, but to avoid creating a new failure scenario, allow for
// that edge case here too.
- var parameterInfos = callback.Delegate?.Method.GetParameters();
- if (parameterInfos == null || parameterInfos.Length == 0)
- {
- return typeof(EventArgs);
- }
- else if (parameterInfos.Length > 1)
- {
- throw new InvalidOperationException($"The event handler for event {eventHandlerId} declares more than one parameter. Only one is supported.");
- }
- else
- {
- var declaredType = parameterInfos[0].ParameterType;
- if (typeof(EventArgs).IsAssignableFrom(declaredType))
- {
- return declaredType;
- }
- else
- {
- throw new InvalidOperationException($"The event handler parameter type {declaredType.FullName} for event must inherit from {typeof(EventArgs).FullName}.");
- }
- }
+ return methodInfo == null
+ ? typeof(EventArgs)
+ : EventArgsTypeCache.GetEventArgsType(methodInfo);
}
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
From 66f7dba0067b72a8606a0ed3408c8865508287d9 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 14:51:17 +0000
Subject: [PATCH 13/25] Create a registry of event types so we'll be able to
add custom ones later
---
.../src/Rendering/Events/EventDelegator.ts | 5 +-
.../{EventArgsFactory.ts => EventTypes.ts} | 256 ++++++++----------
2 files changed, 121 insertions(+), 140 deletions(-)
rename src/Components/Web.JS/src/Rendering/Events/{EventArgsFactory.ts => EventTypes.ts} (60%)
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
index 403d2d667f8e..3ea64147ebd2 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
@@ -1,6 +1,6 @@
-import { createEventArgsFromDOMEvent } from './EventArgsFactory';
import { EventFieldInfo } from './EventFieldInfo';
import { dispatchEvent } from './EventDispatcher';
+import { getEventTypeOptions } from './EventTypes';
const nonBubblingEvents = toLookup([
'abort',
@@ -117,7 +117,8 @@ export class EventDelegator {
if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) {
// We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgsIsPopulated) {
- eventArgs = createEventArgsFromDOMEvent(evt);
+ const eventOptions = getEventTypeOptions(evt.type);
+ eventArgs = eventOptions.createEventArgs(evt);
eventArgsIsPopulated = true;
}
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
similarity index 60%
rename from src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts
rename to src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
index 1f690a4e8ba9..938441cbb772 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
@@ -1,89 +1,64 @@
-export function createEventArgsFromDOMEvent(event: Event): any {
- switch (event.type) {
-
- case 'input':
- case 'change':
- return parseChangeEvent(event);
-
- case 'copy':
- case 'cut':
- case 'paste':
- return {};
-
- case 'drag':
- case 'dragend':
- case 'dragenter':
- case 'dragleave':
- case 'dragover':
- case 'dragstart':
- case 'drop':
- return parseDragEvent(event);
-
- case 'focus':
- case 'blur':
- case 'focusin':
- case 'focusout':
- return {};
-
- case 'keydown':
- case 'keyup':
- case 'keypress':
- return parseKeyboardEvent(event as KeyboardEvent);
-
- case 'contextmenu':
- case 'click':
- case 'mouseover':
- case 'mouseout':
- case 'mousemove':
- case 'mousedown':
- case 'mouseup':
- case 'dblclick':
- return parseMouseEvent(event as MouseEvent);
-
- case 'error':
- return parseErrorEvent(event as ErrorEvent);
-
- case 'loadstart':
- case 'timeout':
- case 'abort':
- case 'load':
- case 'loadend':
- case 'progress':
- return parseProgressEvent(event as ProgressEvent);
-
- case 'touchcancel':
- case 'touchend':
- case 'touchmove':
- case 'touchenter':
- case 'touchleave':
- case 'touchstart':
- return parseTouchEvent(event as TouchEvent);
-
- case 'gotpointercapture':
- case 'lostpointercapture':
- case 'pointercancel':
- case 'pointerdown':
- case 'pointerenter':
- case 'pointerleave':
- case 'pointermove':
- case 'pointerout':
- case 'pointerover':
- case 'pointerup':
- return parsePointerEvent(event as PointerEvent);
-
- case 'wheel':
- case 'mousewheel':
- return parseWheelEvent(event as WheelEvent);
-
- case 'toggle':
- return {};
-
- default:
- return {};
- }
+interface EventTypeOptions {
+ browserEventName?: string;
+ createEventArgs: (event: Event) => any;
+}
+
+const eventTypeRegistry: Map = new Map();
+const emptyEventArgsOptions: EventTypeOptions = {
+ createEventArgs: () => ({})
+};
+
+export function getEventTypeOptions(eventTypeName: string): EventTypeOptions {
+ return eventTypeRegistry.get(eventTypeName) || emptyEventArgsOptions;
+}
+
+function registerBuiltInEventType(eventNames: string[], options: EventTypeOptions) {
+ eventNames.forEach(eventName => eventTypeRegistry.set(eventName, options));
}
-function parseChangeEvent(event: any): ChangeEventArgs {
+registerBuiltInEventType(['input', 'change'], {
+ createEventArgs: parseChangeEvent
+});
+
+registerBuiltInEventType(['copy', 'cut', 'paste'], emptyEventArgsOptions);
+
+registerBuiltInEventType(['drag', 'dragend', 'dragenter', 'dragleave', 'dragover', 'dragstart', 'drop'], {
+ createEventArgs: e => parseDragEvent(e as DragEvent)
+});
+
+registerBuiltInEventType(['focus', 'blur', 'focusin', 'focusout'], emptyEventArgsOptions);
+
+registerBuiltInEventType(['keydown', 'keyup', 'keypress'], {
+ createEventArgs: e => parseKeyboardEvent(e as KeyboardEvent)
+});
+
+registerBuiltInEventType(['contextmenu', 'click', 'mouseover', 'mouseout', 'mousemove', 'mousedown', 'mouseup', 'dblclick'], {
+ createEventArgs: e => parseMouseEvent(e as MouseEvent)
+});
+
+registerBuiltInEventType(['error'], {
+ createEventArgs: e => parseErrorEvent(e as ErrorEvent)
+});
+
+registerBuiltInEventType(['loadstart', 'timeout', 'abort', 'load', 'loadend', 'progress'], {
+ createEventArgs: e => parseProgressEvent(e as ProgressEvent)
+});
+
+registerBuiltInEventType(['touchcancel', 'touchend', 'touchmove', 'touchenter', 'touchleave', 'touchstart'], {
+ createEventArgs: e => parseTouchEvent(e as TouchEvent)
+});
+
+registerBuiltInEventType(['gotpointercapture', 'lostpointercapture', 'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup'], {
+ createEventArgs: e => parsePointerEvent(e as PointerEvent)
+});
+
+registerBuiltInEventType(['wheel', 'mousewheel'], {
+ createEventArgs: e => parseWheelEvent(e as WheelEvent)
+});
+
+registerBuiltInEventType(['toggle'], emptyEventArgsOptions);
+
+function parseChangeEvent(event: Event): ChangeEventArgs {
const element = event.target as Element;
if (isTimeBasedInput(element)) {
const normalizedValue = normalizeTimeBasedValue(element);
@@ -95,13 +70,6 @@ function parseChangeEvent(event: any): ChangeEventArgs {
}
}
-function parseDragEvent(event: any): DragEventArgs {
- return {
- ...parseMouseEvent(event),
- dataTransfer: event.dataTransfer,
- };
-}
-
function parseWheelEvent(event: WheelEvent): WheelEventArgs {
return {
...parseMouseEvent(event),
@@ -112,43 +80,21 @@ function parseWheelEvent(event: WheelEvent): WheelEventArgs {
};
}
-function parseErrorEvent(event: ErrorEvent): ErrorEventArgs {
- return {
- message: event.message,
- filename: event.filename,
- lineno: event.lineno,
- colno: event.colno,
- };
-}
-
-function parseProgressEvent(event: ProgressEvent): ProgressEventArgs {
+function parsePointerEvent(event: PointerEvent): PointerEventArgs {
return {
- lengthComputable: event.lengthComputable,
- loaded: event.loaded,
- total: event.total,
+ ...parseMouseEvent(event),
+ pointerId: event.pointerId,
+ width: event.width,
+ height: event.height,
+ pressure: event.pressure,
+ tiltX: event.tiltX,
+ tiltY: event.tiltY,
+ pointerType: event.pointerType,
+ isPrimary: event.isPrimary,
};
}
function parseTouchEvent(event: TouchEvent): TouchEventArgs {
-
- function parseTouch(touchList: TouchList) {
- const touches: TouchPoint[] = [];
-
- for (let i = 0; i < touchList.length; i++) {
- const touch = touchList[i];
- touches.push({
- identifier: touch.identifier,
- clientX: touch.clientX,
- clientY: touch.clientY,
- screenX: touch.screenX,
- screenY: touch.screenY,
- pageX: touch.pageX,
- pageY: touch.pageY,
- });
- }
- return touches;
- }
-
return {
detail: event.detail,
touches: parseTouch(event.touches),
@@ -161,6 +107,23 @@ function parseTouchEvent(event: TouchEvent): TouchEventArgs {
};
}
+function parseProgressEvent(event: ProgressEvent): ProgressEventArgs {
+ return {
+ lengthComputable: event.lengthComputable,
+ loaded: event.loaded,
+ total: event.total,
+ };
+}
+
+function parseErrorEvent(event: ErrorEvent): ErrorEventArgs {
+ return {
+ message: event.message,
+ filename: event.filename,
+ lineno: event.lineno,
+ colno: event.colno,
+ };
+}
+
function parseKeyboardEvent(event: KeyboardEvent): KeyboardEventArgs {
return {
key: event.key,
@@ -174,20 +137,37 @@ function parseKeyboardEvent(event: KeyboardEvent): KeyboardEventArgs {
};
}
-function parsePointerEvent(event: PointerEvent): PointerEventArgs {
+function parseDragEvent(event: DragEvent): DragEventArgs {
return {
...parseMouseEvent(event),
- pointerId: event.pointerId,
- width: event.width,
- height: event.height,
- pressure: event.pressure,
- tiltX: event.tiltX,
- tiltY: event.tiltY,
- pointerType: event.pointerType,
- isPrimary: event.isPrimary,
+ dataTransfer: event.dataTransfer ? {
+ dropEffect: event.dataTransfer.dropEffect,
+ effectAllowed: event.dataTransfer.effectAllowed,
+ files: Array.from(event.dataTransfer.files).map(f => f.name),
+ items: Array.from(event.dataTransfer.items).map(i => ({ kind: i.kind, type: i.type })),
+ types: event.dataTransfer.types,
+ } : null,
};
}
+function parseTouch(touchList: TouchList): TouchPoint[] {
+ const touches: TouchPoint[] = [];
+
+ for (let i = 0; i < touchList.length; i++) {
+ const touch = touchList[i];
+ touches.push({
+ identifier: touch.identifier,
+ clientX: touch.clientX,
+ clientY: touch.clientY,
+ screenX: touch.screenX,
+ screenY: touch.screenY,
+ pageX: touch.pageX,
+ pageY: touch.pageY,
+ });
+ }
+ return touches;
+}
+
function parseMouseEvent(event: MouseEvent): MouseEventArgs {
return {
detail: event.detail,
@@ -240,7 +220,7 @@ function normalizeTimeBasedValue(element: HTMLInputElement): string {
throw new Error(`Invalid element type '${type}'.`);
}
-// The following interfaces must be kept in sync with the UIEventArgs C# classes
+// The following interfaces must be kept in sync with the EventArgs C# classes
interface ChangeEventArgs {
value: string | boolean;
@@ -248,7 +228,7 @@ interface ChangeEventArgs {
interface DragEventArgs {
detail: number;
- dataTransfer: DataTransfer;
+ dataTransfer: DataTransferEventArgs | null;
screenX: number;
screenY: number;
clientX: number;
@@ -261,12 +241,12 @@ interface DragEventArgs {
metaKey: boolean;
}
-interface DataTransfer {
+interface DataTransferEventArgs {
dropEffect: string;
effectAllowed: string;
- files: string[];
- items: DataTransferItem[];
- types: string[];
+ files: readonly string[];
+ items: readonly DataTransferItem[];
+ types: readonly string[];
}
interface DataTransferItem {
From c9ca4b8cced7c1528395d6474be2fb463e2b7e6e Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Tue, 9 Feb 2021 15:02:30 +0000
Subject: [PATCH 14/25] Public API for registering custom event types
---
src/Components/Web.JS/src/GlobalExports.ts | 2 ++
.../src/Rendering/Events/EventDelegator.ts | 2 +-
.../Web.JS/src/Rendering/Events/EventTypes.ts | 24 ++++++++++++++-----
3 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts
index facb2eea445d..1443192e477a 100644
--- a/src/Components/Web.JS/src/GlobalExports.ts
+++ b/src/Components/Web.JS/src/GlobalExports.ts
@@ -1,10 +1,12 @@
import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
import { domFunctions } from './DomWrapper';
import { Virtualize } from './Virtualize';
+import { registerCustomEventType } from './Rendering/Events/EventTypes';
// Make the following APIs available in global scope for invocation from JS
window['Blazor'] = {
navigateTo,
+ registerCustomEventType,
_internal: {
navigationManager: navigationManagerInternalFunctions,
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
index 3ea64147ebd2..30e9f8af96c7 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
@@ -118,7 +118,7 @@ export class EventDelegator {
// We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgsIsPopulated) {
const eventOptions = getEventTypeOptions(evt.type);
- eventArgs = eventOptions.createEventArgs(evt);
+ eventArgs = eventOptions.createEventArgs ? eventOptions.createEventArgs(evt) : {};
eventArgsIsPopulated = true;
}
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
index 938441cbb772..f641b814d5cc 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
@@ -1,15 +1,27 @@
interface EventTypeOptions {
browserEventName?: string;
- createEventArgs: (event: Event) => any;
+ createEventArgs?: (event: Event) => any;
}
const eventTypeRegistry: Map = new Map();
-const emptyEventArgsOptions: EventTypeOptions = {
- createEventArgs: () => ({})
-};
+const emptyEventArgsOptions: EventTypeOptions = {};
-export function getEventTypeOptions(eventTypeName: string): EventTypeOptions {
- return eventTypeRegistry.get(eventTypeName) || emptyEventArgsOptions;
+export function registerCustomEventType(eventName: string, options: EventTypeOptions): void {
+ if (!options) {
+ throw new Error('The options parameter is required.');
+ }
+
+ // There can't be more than one registration for the same event name because then we wouldn't
+ // know which eventargs data to supply.
+ if (eventTypeRegistry.has(eventName)) {
+ throw new Error(`The event '${eventName}' is already registered.`);
+ }
+
+ eventTypeRegistry.set(eventName, options);
+}
+
+export function getEventTypeOptions(eventName: string): EventTypeOptions {
+ return eventTypeRegistry.get(eventName) || emptyEventArgsOptions;
}
function registerBuiltInEventType(eventNames: string[], options: EventTypeOptions) {
From 11cbd2035004ef1511fd954a73e17069b106f349 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Wed, 10 Feb 2021 11:23:32 +0000
Subject: [PATCH 15/25] Dispatch events to any registered event type aliases
too
---
.../src/Rendering/Events/EventDelegator.ts | 58 ++++++++++++-------
.../Web.JS/src/Rendering/Events/EventTypes.ts | 28 +++++++--
2 files changed, 59 insertions(+), 27 deletions(-)
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
index 30e9f8af96c7..1683f1f74547 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
@@ -1,6 +1,6 @@
import { EventFieldInfo } from './EventFieldInfo';
import { dispatchEvent } from './EventDispatcher';
-import { getEventTypeOptions } from './EventTypes';
+import { getEventNameAliases, getEventTypeOptions } from './EventTypes';
const nonBubblingEvents = toLookup([
'abort',
@@ -104,54 +104,68 @@ export class EventDelegator {
return;
}
+ // Always dispatch to any listeners for the original underlying browser event name
+ this.dispatchGlobalEventToAllElements(evt.type, evt);
+
+ // If this event name has aliases, dispatch for those listeners too
+ const eventNameAliases = getEventNameAliases(evt.type);
+ eventNameAliases && eventNameAliases.forEach(alias =>
+ this.dispatchGlobalEventToAllElements(alias, evt));
+
+ // Special case for navigation interception
+ if (evt.type === 'click') {
+ this.afterClickCallbacks.forEach(callback => callback(evt as MouseEvent));
+ }
+ }
+
+ private dispatchGlobalEventToAllElements(eventName: string, browserEvent: Event) {
+ // Note that 'eventName' can be an alias. For example, eventName may be 'click.special'
+ // while browserEvent.type may be 'click'.
+
// Scan up the element hierarchy, looking for any matching registered event handlers
- let candidateElement = evt.target as Element | null;
+ let candidateElement = browserEvent.target as Element | null;
let eventArgs: any = null; // Populate lazily
let eventArgsIsPopulated = false;
- const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
+ const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(eventName);
let stopPropagationWasRequested = false;
while (candidateElement) {
const handlerInfos = this.getEventHandlerInfosForElement(candidateElement, false);
if (handlerInfos) {
- const handlerInfo = handlerInfos.getHandler(evt.type);
- if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) {
+ const handlerInfo = handlerInfos.getHandler(eventName);
+ if (handlerInfo && !eventIsDisabledOnElement(candidateElement, browserEvent.type)) {
// We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgsIsPopulated) {
- const eventOptions = getEventTypeOptions(evt.type);
- eventArgs = eventOptions.createEventArgs ? eventOptions.createEventArgs(evt) : {};
+ const eventOptions = getEventTypeOptions(eventName);
+ eventArgs = eventOptions.createEventArgs ? eventOptions.createEventArgs(browserEvent) : null;
eventArgsIsPopulated = true;
}
// For certain built-in events, having any .NET handler implicitly means we will prevent
- // the browser's default behavior
- if (alwaysPreventDefaultEvents.hasOwnProperty(evt.type)) {
- evt.preventDefault();
+ // the browser's default behavior. This has to be based on the original browser event type name,
+ // not any alias (e.g., if you create a custom 'submit' variant, it should still preventDefault).
+ if (alwaysPreventDefaultEvents.hasOwnProperty(browserEvent.type)) {
+ browserEvent.preventDefault();
}
dispatchEvent({
browserRendererId: this.browserRendererId,
eventHandlerId: handlerInfo.eventHandlerId,
- eventName: evt.type,
- eventFieldInfo: EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt)
+ eventName: eventName,
+ eventFieldInfo: EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, browserEvent)
}, eventArgs);
}
- if (handlerInfos.stopPropagation(evt.type)) {
+ if (handlerInfos.stopPropagation(eventName)) {
stopPropagationWasRequested = true;
}
- if (handlerInfos.preventDefault(evt.type)) {
- evt.preventDefault();
+ if (handlerInfos.preventDefault(eventName)) {
+ browserEvent.preventDefault();
}
}
candidateElement = (eventIsNonBubbling || stopPropagationWasRequested) ? null : candidateElement.parentElement;
}
-
- // Special case for navigation interception
- if (evt.type === 'click') {
- this.afterClickCallbacks.forEach(callback => callback(evt as MouseEvent));
- }
}
private getEventHandlerInfosForElement(element: Element, createIfNeeded: boolean): EventHandlerInfosForElement | null {
@@ -293,10 +307,10 @@ function toLookup(items: string[]): { [key: string]: boolean } {
return result;
}
-function eventIsDisabledOnElement(element: Element, eventName: string): boolean {
+function eventIsDisabledOnElement(element: Element, rawBrowserEventName: string): boolean {
// We want to replicate the normal DOM event behavior that, for 'interactive' elements
// with a 'disabled' attribute, certain mouse events are suppressed
return (element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)
- && disableableEventNames.hasOwnProperty(eventName)
+ && disableableEventNames.hasOwnProperty(rawBrowserEventName)
&& element.disabled;
}
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
index f641b814d5cc..a99b31dc5045 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
@@ -4,7 +4,9 @@ interface EventTypeOptions {
}
const eventTypeRegistry: Map = new Map();
-const emptyEventArgsOptions: EventTypeOptions = {};
+const browserEventNamesToAliases: Map = new Map();
+const unknownEventTypeOptions: EventTypeOptions = { };
+const createBlankEventArgsOptions: EventTypeOptions = { createEventArgs: () => ({}) };
export function registerCustomEventType(eventName: string, options: EventTypeOptions): void {
if (!options) {
@@ -17,11 +19,27 @@ export function registerCustomEventType(eventName: string, options: EventTypeOpt
throw new Error(`The event '${eventName}' is already registered.`);
}
+ // If applicable, register this as an alias of the given browserEventName
+ if (options.browserEventName) {
+ const aliasGroup = browserEventNamesToAliases.get(options.browserEventName);
+ if (aliasGroup) {
+ aliasGroup.push(eventName);
+ } else {
+ browserEventNamesToAliases.set(options.browserEventName, [eventName]);
+ }
+
+ // Make sure there's a global delegating handler for that browserEventName
+ }
+
eventTypeRegistry.set(eventName, options);
}
export function getEventTypeOptions(eventName: string): EventTypeOptions {
- return eventTypeRegistry.get(eventName) || emptyEventArgsOptions;
+ return eventTypeRegistry.get(eventName) || unknownEventTypeOptions;
+}
+
+export function getEventNameAliases(eventName: string): string[] | undefined {
+ return browserEventNamesToAliases.get(eventName);
}
function registerBuiltInEventType(eventNames: string[], options: EventTypeOptions) {
@@ -32,13 +50,13 @@ registerBuiltInEventType(['input', 'change'], {
createEventArgs: parseChangeEvent
});
-registerBuiltInEventType(['copy', 'cut', 'paste'], emptyEventArgsOptions);
+registerBuiltInEventType(['copy', 'cut', 'paste'], createBlankEventArgsOptions);
registerBuiltInEventType(['drag', 'dragend', 'dragenter', 'dragleave', 'dragover', 'dragstart', 'drop'], {
createEventArgs: e => parseDragEvent(e as DragEvent)
});
-registerBuiltInEventType(['focus', 'blur', 'focusin', 'focusout'], emptyEventArgsOptions);
+registerBuiltInEventType(['focus', 'blur', 'focusin', 'focusout'], createBlankEventArgsOptions);
registerBuiltInEventType(['keydown', 'keyup', 'keypress'], {
createEventArgs: e => parseKeyboardEvent(e as KeyboardEvent)
@@ -68,7 +86,7 @@ registerBuiltInEventType(['wheel', 'mousewheel'], {
createEventArgs: e => parseWheelEvent(e as WheelEvent)
});
-registerBuiltInEventType(['toggle'], emptyEventArgsOptions);
+registerBuiltInEventType(['toggle'], createBlankEventArgsOptions);
function parseChangeEvent(event: Event): ChangeEventArgs {
const element = event.target as Element;
From 6ca5e3a888832a44555c896bf896721ededfed50 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Wed, 10 Feb 2021 11:39:36 +0000
Subject: [PATCH 16/25] Back-compat for unregistered event types
---
.../Web.JS/src/Rendering/Events/EventDelegator.ts | 8 ++++++--
src/Components/Web.JS/src/Rendering/Events/EventTypes.ts | 5 ++---
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
index 1683f1f74547..ed546ae8ca56 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
@@ -135,8 +135,12 @@ export class EventDelegator {
if (handlerInfo && !eventIsDisabledOnElement(candidateElement, browserEvent.type)) {
// We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgsIsPopulated) {
- const eventOptions = getEventTypeOptions(eventName);
- eventArgs = eventOptions.createEventArgs ? eventOptions.createEventArgs(browserEvent) : null;
+ const eventOptionsIfRegistered = getEventTypeOptions(eventName);
+ // For back-compat, if there's no registered createEventArgs, we supply empty event args (not null).
+ // But if there is a registered createEventArgs, it can supply anything (including null).
+ eventArgs = eventOptionsIfRegistered?.createEventArgs
+ ? eventOptionsIfRegistered.createEventArgs(browserEvent)
+ : {};
eventArgsIsPopulated = true;
}
diff --git a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
index a99b31dc5045..71abd941363c 100644
--- a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
+++ b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
@@ -5,7 +5,6 @@ interface EventTypeOptions {
const eventTypeRegistry: Map = new Map();
const browserEventNamesToAliases: Map = new Map();
-const unknownEventTypeOptions: EventTypeOptions = { };
const createBlankEventArgsOptions: EventTypeOptions = { createEventArgs: () => ({}) };
export function registerCustomEventType(eventName: string, options: EventTypeOptions): void {
@@ -34,8 +33,8 @@ export function registerCustomEventType(eventName: string, options: EventTypeOpt
eventTypeRegistry.set(eventName, options);
}
-export function getEventTypeOptions(eventName: string): EventTypeOptions {
- return eventTypeRegistry.get(eventName) || unknownEventTypeOptions;
+export function getEventTypeOptions(eventName: string): EventTypeOptions | undefined {
+ return eventTypeRegistry.get(eventName);
}
export function getEventNameAliases(eventName: string): string[] | undefined {
From c1523ff16e71358fa7cfae90dd1d9884dd38ff83 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Wed, 10 Feb 2021 12:05:38 +0000
Subject: [PATCH 17/25] Update some older E2E tests
---
.../BasicTestApp/EventBubblingComponent.razor | 13 ++++++-------
.../BasicTestApp/EventPreventDefaultComponent.razor | 4 ----
2 files changed, 6 insertions(+), 11 deletions(-)
diff --git a/src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor b/src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor
index 66c28ac823fa..87343158bad1 100644
--- a/src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor
@@ -1,10 +1,9 @@

Bubbling standard event
-@* Temporarily hard-coding the internal names - this will be replaced once the Razor compiler supports @onevent:stopPropagation and @onevent:preventDefault *@
LogEvent("parent onclick"))">
@* This element shows you can stop propagation even without necessarily also handling the event *@
-