ソースを参照

Custom event args (#29993)

* Mark redundant extension methods as obsolete.

* Add E2E test showing polymorphic event handlers work today

... because a subsequent implementation change would break this if it wasn't accounted for

* 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

* When necessary, parse the event args JSON using the parameter type from the handler delegate

* Fix event mapping

* Simplify event dispatching on TypeScript side more. Eliminate unnecessary types.

* Rename file for clarity

* Migrate "preventDefault for submit" behavior into EventDelegator because that's where other similar responsibilities are

* Put event-centric files into an Events directory for more clarity

* Disentangle event dispatch from BrowserRenderer

* Update comment

* Add a cache for the handler->argstype lookup

* Create a registry of event types so we'll be able to add custom ones later

* Public API for registering custom event types

* Dispatch events to any registered event type aliases too

* Back-compat for unregistered event types

* Update some older E2E tests

* Begin on E2E scenarios

* More E2E scenarios

* Change E2E scenario to use keydown, not paste, to avoid isolation complications during automated runs

* Prepare E2E case for when an aliased event has no native global listener

* Support custom events that have no corresponding native global listener

* E2E test cases

* Another test case showing multiple aliases work

* Update blazor.server.js
Steve Sanderson 5 年 前
コミット
d97be901b5
32 ファイル変更1184 行追加508 行削除
  1. 1 0
      src/Components/Components/src/PublicAPI.Unshipped.txt
  2. 42 0
      src/Components/Components/src/RenderTree/EventArgsTypeCache.cs
  3. 29 5
      src/Components/Components/src/RenderTree/Renderer.cs
  4. 101 0
      src/Components/Components/test/RendererTest.cs
  5. 1 1
      src/Components/Server/src/Circuits/CircuitHost.cs
  6. 132 23
      src/Components/Shared/src/WebEventData.cs
  7. 0 0
      src/Components/Web.JS/dist/Release/blazor.server.js
  8. 1 1
      src/Components/Web.JS/src/Boot.Server.ts
  9. 1 1
      src/Components/Web.JS/src/Boot.WebAssembly.ts
  10. 2 0
      src/Components/Web.JS/src/GlobalExports.ts
  11. 2 39
      src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
  12. 0 378
      src/Components/Web.JS/src/Rendering/EventForDotNet.ts
  13. 78 26
      src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts
  14. 11 5
      src/Components/Web.JS/src/Rendering/Events/EventDispatcher.ts
  15. 0 0
      src/Components/Web.JS/src/Rendering/Events/EventFieldInfo.ts
  16. 377 0
      src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
  17. 1 1
      src/Components/Web.JS/src/Services/NavigationManager.ts
  18. 4 0
      src/Components/Web/src/PublicAPI.Unshipped.txt
  19. 20 0
      src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs
  20. 1 1
      src/Components/Web/src/WebEventDescriptor.cs
  21. 1 1
      src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs
  22. 2 2
      src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs
  23. 2 2
      src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs
  24. 8 0
      src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs
  25. 179 0
      src/Components/test/E2ETest/Tests/EventCustomArgsTest.cs
  26. 25 1
      src/Components/test/E2ETest/Tests/EventTest.cs
  27. 6 7
      src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor
  28. 102 0
      src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor
  29. 28 0
      src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs
  30. 0 4
      src/Components/test/testassets/BasicTestApp/EventPreventDefaultComponent.razor
  31. 1 1
      src/Components/test/testassets/BasicTestApp/Index.razor
  32. 26 9
      src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor

+ 1 - 0
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<string!, object?>! 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

+ 42 - 0
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<MethodInfo, Type> Cache = new ConcurrentDictionary<MethodInfo, Type>();
+
+        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}.");
+                    }
+                }
+            });
+        }
+    }
+}

+ 29 - 5
src/Components/Components/src/RenderTree/Renderer.cs

@@ -249,11 +249,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
         {
             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,24 @@ namespace Microsoft.AspNetCore.Components.RenderTree
             return result;
         }
 
+        /// <summary>
+        /// Gets the event arguments type for the specified event handler.
+        /// </summary>
+        /// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
+        /// <returns>The parameter type expected by the event handler. Normally this is a subclass of <see cref="EventArgs"/>.</returns>
+        public Type GetEventArgsType(ulong 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.
+            return methodInfo == null
+                ? typeof(EventArgs)
+                : EventArgsTypeCache.GetEventArgsType(methodInfo);
+        }
+
         internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
         {
             if (frame.FrameTypeField != RenderTreeFrameType.Component)
@@ -404,6 +418,16 @@ namespace Microsoft.AspNetCore.Components.RenderTree
             _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))

+ 101 - 0
src/Components/Components/test/RendererTest.cs

@@ -484,6 +484,98 @@ namespace Microsoft.AspNetCore.Components.Test
             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<DerivedEventArgs, Task>)(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>)(() => 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<EventArgs, string>)((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<InvalidOperationException>(() => 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<DateTime>)(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<InvalidOperationException>(() => renderer.GetEventArgsType(eventHandlerId));
+            Assert.Contains($"must inherit from {typeof(EventArgs).FullName}", ex.Message);
+        }
+
         [Fact]
         public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers()
         {
@@ -4224,6 +4316,9 @@ namespace Microsoft.AspNetCore.Components.Test
             [Parameter]
             public EventCallback<DerivedEventArgs> OnClickEventCallbackOfT { get; set; }
 
+            [Parameter]
+            public Delegate OnArbitraryDelegateEvent { get; set; }
+
             public bool SkipElement { get; set; }
             private int renderCount = 0;
 
@@ -4269,6 +4364,12 @@ namespace Microsoft.AspNetCore.Components.Test
                     {
                         builder.AddAttribute(5, "onclickaction", OnClickAsyncAction);
                     }
+
+                    if (OnArbitraryDelegateEvent != null)
+                    {
+                        builder.AddAttribute(6, "onarbitrarydelegateevent", OnArbitraryDelegateEvent);
+                    }
+
                     builder.CloseElement();
                     builder.CloseElement();
                 }

+ 1 - 1
src/Components/Server/src/Circuits/CircuitHost.cs

@@ -399,7 +399,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             WebEventData webEventData;
             try
             {
-                webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson);
+                webEventData = WebEventData.Parse(Renderer, eventDescriptorJson, eventArgsJson);
             }
             catch (Exception ex)
             {

+ 132 - 23
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 @@ namespace Microsoft.AspNetCore.Components.Web
             }
 
             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 @@ namespace Microsoft.AspNetCore.Components.Web
 
         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<ClipboardEventArgs>(eventArgsJson),
-                    "drag" => Deserialize<DragEventArgs>(eventArgsJson),
-                    "error" => Deserialize<ErrorEventArgs>(eventArgsJson),
-                    "focus" => Deserialize<FocusEventArgs>(eventArgsJson),
-                    "keyboard" => Deserialize<KeyboardEventArgs>(eventArgsJson),
-                    "mouse" => Deserialize<MouseEventArgs>(eventArgsJson),
-                    "pointer" => Deserialize<PointerEventArgs>(eventArgsJson),
-                    "progress" => Deserialize<ProgressEventArgs>(eventArgsJson),
-                    "touch" => Deserialize<TouchEventArgs>(eventArgsJson),
-                    "unknown" => EventArgs.Empty,
-                    "wheel" => Deserialize<WheelEventArgs>(eventArgsJson),
-                    "toggle" => Deserialize<EventArgs>(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<T>(string json) => JsonSerializer.Deserialize<T>(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 EventArgsFactory.ts.
+
+            switch (eventName)
+            {
+                case "input":
+                case "change":
+                    type = typeof(ChangeEventArgs);
+                    return true;
+
+                case "copy":
+                case "cut":
+                case "paste":
+                    type = typeof(ClipboardEventArgs);
+                    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(FocusEventArgs);
+                    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 @@ namespace Microsoft.AspNetCore.Components.Web
             return null;
         }
 
+        static T Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializerOptionsProvider.Options)!;
+
         private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
         {
             var changeArgs = Deserialize<ChangeEventArgs>(eventArgsJson);

ファイルの差分が大きいため隠しています
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.server.js


+ 1 - 1
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';

+ 1 - 1
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';

+ 2 - 0
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,

+ 2 - 39
src/Components/Web.JS/src/Rendering/BrowserRenderer.ts

@@ -1,15 +1,11 @@
 import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
-import { EventDelegator } from './EventDelegator';
-import { EventForDotNet, UIEventArgs, EventArgsType } from './EventForDotNet';
+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 { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
 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,13 +16,8 @@ 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(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
@@ -459,13 +450,6 @@ export interface ComponentDescriptor {
   end: Node;
 }
 
-export interface EventDescriptor {
-  browserRendererId: number;
-  eventHandlerId: number;
-  eventArgsType: EventArgsType;
-  eventFieldInfo: EventFieldInfo | null;
-}
-
 function parseMarkup(markup: string, isSvg: boolean) {
   if (isSvg) {
     sharedSvgElemForParsing.innerHTML = markup || ' ';
@@ -491,27 +475,6 @@ function countDescendantFrames(batch: RenderBatch, frame: RenderTreeFrame): numb
   }
 }
 
-function raiseEvent(
-  event: Event,
-  browserRendererId: number,
-  eventHandlerId: number,
-  eventArgs: EventForDotNet<UIEventArgs>,
-  eventFieldInfo: EventFieldInfo | null
-): void {
-  if (preventDefaultEvents[event.type]) {
-    event.preventDefault();
-  }
-
-  const eventDescriptor = {
-    browserRendererId,
-    eventHandlerId,
-    eventArgsType: eventArgs.type,
-    eventFieldInfo: eventFieldInfo,
-  };
-
-  dispatchEvent(eventDescriptor, eventArgs.data);
-}
-
 function clearElement(element: Element) {
   let childNode: Node | null;
   while (childNode = element.firstChild) {

+ 0 - 378
src/Components/Web.JS/src/Rendering/EventForDotNet.ts

@@ -1,378 +0,0 @@
-export class EventForDotNet<TData extends UIEventArgs> {
-  public constructor(public readonly type: EventArgsType, public readonly data: TData) {
-  }
-
-  public static fromDOMEvent(event: Event): EventForDotNet<UIEventArgs> {
-    const element = event.target as Element;
-    switch (event.type) {
-
-      case 'input':
-      case 'change': {
-
-        if (isTimeBasedInput(element)) {
-          const normalizedValue = normalizeTimeBasedValue(element);
-          return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: normalizedValue });
-        }
-
-        const targetIsCheckbox = isCheckbox(element);
-        const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
-        return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: newValue });
-      }
-
-      case 'copy':
-      case 'cut':
-      case 'paste':
-        return new EventForDotNet<UIClipboardEventArgs>('clipboard', { type: event.type });
-
-      case 'drag':
-      case 'dragend':
-      case 'dragenter':
-      case 'dragleave':
-      case 'dragover':
-      case 'dragstart':
-      case 'drop':
-        return new EventForDotNet<UIDragEventArgs>('drag', parseDragEvent(event));
-
-      case 'focus':
-      case 'blur':
-      case 'focusin':
-      case 'focusout':
-        return new EventForDotNet<UIFocusEventArgs>('focus', { type: event.type });
-
-      case 'keydown':
-      case 'keyup':
-      case 'keypress':
-        return new EventForDotNet<UIKeyboardEventArgs>('keyboard', parseKeyboardEvent(event as KeyboardEvent));
-
-      case 'contextmenu':
-      case 'click':
-      case 'mouseover':
-      case 'mouseout':
-      case 'mousemove':
-      case 'mousedown':
-      case 'mouseup':
-      case 'dblclick':
-        return new EventForDotNet<UIMouseEventArgs>('mouse', parseMouseEvent(event as MouseEvent));
-
-      case 'error':
-        return new EventForDotNet<UIErrorEventArgs>('error', parseErrorEvent(event as ErrorEvent));
-
-      case 'loadstart':
-      case 'timeout':
-      case 'abort':
-      case 'load':
-      case 'loadend':
-      case 'progress':
-        return new EventForDotNet<UIProgressEventArgs>('progress', parseProgressEvent(event as ProgressEvent));
-
-      case 'touchcancel':
-      case 'touchend':
-      case 'touchmove':
-      case 'touchenter':
-      case 'touchleave':
-      case 'touchstart':
-        return new EventForDotNet<UITouchEventArgs>('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<UIPointerEventArgs>('pointer', parsePointerEvent(event as PointerEvent));
-
-      case 'wheel':
-      case 'mousewheel':
-        return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(event as WheelEvent));
-
-      case 'toggle':
-        return new EventForDotNet<UIEventArgs>('toggle', { type: event.type });
-
-      default:
-        return new EventForDotNet<UIEventArgs>('unknown', { type: event.type });
-    }
-  }
-}
-
-function parseDragEvent(event: any) {
-  return {
-    ...parseMouseEvent(event),
-    dataTransfer: event.dataTransfer,
-
-  };
-}
-
-function parseWheelEvent(event: WheelEvent) {
-  return {
-    ...parseMouseEvent(event),
-    deltaX: event.deltaX,
-    deltaY: event.deltaY,
-    deltaZ: event.deltaZ,
-    deltaMode: event.deltaMode,
-  };
-}
-
-function parseErrorEvent(event: ErrorEvent) {
-  return {
-    type: event.type,
-    message: event.message,
-    filename: event.filename,
-    lineno: event.lineno,
-    colno: event.colno,
-  };
-}
-
-function parseProgressEvent(event: ProgressEvent) {
-  return {
-    type: event.type,
-    lengthComputable: event.lengthComputable,
-    loaded: event.loaded,
-    total: event.total,
-  };
-}
-
-function parseTouchEvent(event: TouchEvent) {
-
-  function parseTouch(touchList: TouchList) {
-    const touches: UITouchPoint[] = [];
-
-    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 {
-    type: event.type,
-    detail: event.detail,
-    touches: parseTouch(event.touches),
-    targetTouches: parseTouch(event.targetTouches),
-    changedTouches: parseTouch(event.changedTouches),
-    ctrlKey: event.ctrlKey,
-    shiftKey: event.shiftKey,
-    altKey: event.altKey,
-    metaKey: event.metaKey,
-  };
-}
-
-function parseKeyboardEvent(event: KeyboardEvent) {
-  return {
-    type: event.type,
-    key: event.key,
-    code: event.code,
-    location: event.location,
-    repeat: event.repeat,
-    ctrlKey: event.ctrlKey,
-    shiftKey: event.shiftKey,
-    altKey: event.altKey,
-    metaKey: event.metaKey,
-  };
-}
-
-function parsePointerEvent(event: PointerEvent) {
-  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,
-  };
-}
-
-function parseMouseEvent(event: MouseEvent) {
-  return {
-    type: event.type,
-    detail: event.detail,
-    screenX: event.screenX,
-    screenY: event.screenY,
-    clientX: event.clientX,
-    clientY: event.clientY,
-    offsetX: event.offsetX,
-    offsetY: event.offsetY,
-    button: event.button,
-    buttons: event.buttons,
-    ctrlKey: event.ctrlKey,
-    shiftKey: event.shiftKey,
-    altKey: event.altKey,
-    metaKey: event.metaKey,
-  };
-}
-
-function isCheckbox(element: Element | null): boolean {
-  return !!element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
-}
-
-const timeBasedInputs = [
-  'date',
-  'datetime-local',
-  'month',
-  'time',
-  'week',
-];
-
-function isTimeBasedInput(element: Element): element is HTMLInputElement {
-  return timeBasedInputs.indexOf(element.getAttribute('type')!) !== -1;
-}
-
-function normalizeTimeBasedValue(element: HTMLInputElement): string {
-  const value = element.value;
-  const type = element.type;
-  switch (type) {
-    case 'date':
-    case 'datetime-local':
-    case 'month':
-      return value;
-    case 'time':
-      return value.length === 5 ? value + ':00' : value; // Convert hh:mm to hh:mm:00
-    case 'week':
-      // For now we are not going to normalize input type week as it is not trivial
-      return value;
-  }
-
-  throw new Error(`Invalid element type '${type}'.`);
-}
-
-// 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;
-}
-
-interface UIChangeEventArgs extends UIEventArgs {
-  value: string | boolean;
-}
-
-interface UIClipboardEventArgs extends UIEventArgs {
-}
-
-interface UIDragEventArgs extends UIEventArgs {
-  detail: number;
-  dataTransfer: UIDataTransfer;
-  screenX: number;
-  screenY: number;
-  clientX: number;
-  clientY: number;
-  button: number;
-  buttons: number;
-  ctrlKey: boolean;
-  shiftKey: boolean;
-  altKey: boolean;
-  metaKey: boolean;
-}
-
-interface UIDataTransfer {
-  dropEffect: string;
-  effectAllowed: string;
-  files: string[];
-  items: UIDataTransferItem[];
-  types: string[];
-}
-
-interface UIDataTransferItem {
-  kind: string;
-  type: string;
-}
-
-interface UIErrorEventArgs extends UIEventArgs {
-  message: string;
-  filename: string;
-  lineno: number;
-  colno: number;
-
-  // omitting 'error' here since we'd have to serialize it, and it's not clear we will want to
-  // do that. https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
-}
-
-interface UIFocusEventArgs extends UIEventArgs {
-}
-
-interface UIKeyboardEventArgs extends UIEventArgs {
-  key: string;
-  code: string;
-  location: number;
-  repeat: boolean;
-  ctrlKey: boolean;
-  shiftKey: boolean;
-  altKey: boolean;
-  metaKey: boolean;
-}
-
-interface UIMouseEventArgs extends UIEventArgs {
-  detail: number;
-  screenX: number;
-  screenY: number;
-  clientX: number;
-  clientY: number;
-  offsetX: number;
-  offsetY: number;
-  button: number;
-  buttons: number;
-  ctrlKey: boolean;
-  shiftKey: boolean;
-  altKey: boolean;
-  metaKey: boolean;
-}
-
-interface UIPointerEventArgs extends UIMouseEventArgs {
-  pointerId: number;
-  width: number;
-  height: number;
-  pressure: number;
-  tiltX: number;
-  tiltY: number;
-  pointerType: string;
-  isPrimary: boolean;
-}
-
-interface UIProgressEventArgs extends UIEventArgs {
-  lengthComputable: boolean;
-  loaded: number;
-  total: number;
-}
-
-interface UITouchEventArgs extends UIEventArgs {
-  detail: number;
-  touches: UITouchPoint[];
-  targetTouches: UITouchPoint[];
-  changedTouches: UITouchPoint[];
-  ctrlKey: boolean;
-  shiftKey: boolean;
-  altKey: boolean;
-  metaKey: boolean;
-}
-
-interface UITouchPoint {
-  identifier: number;
-  screenX: number;
-  screenY: number;
-  clientX: number;
-  clientY: number;
-  pageX: number;
-  pageY: number;
-}
-
-interface UIWheelEventArgs extends UIMouseEventArgs {
-  deltaX: number;
-  deltaY: number;
-  deltaZ: number;
-  deltaMode: number;
-}

+ 78 - 26
src/Components/Web.JS/src/Rendering/EventDelegator.ts → src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts

@@ -1,5 +1,6 @@
-import { EventForDotNet, UIEventArgs } from './EventForDotNet';
 import { EventFieldInfo } from './EventFieldInfo';
+import { dispatchEvent } from './EventDispatcher';
+import { eventNameAliasRegisteredCallbacks, getBrowserEventName, getEventNameAliases, getEventTypeOptions } from './EventTypes';
 
 const nonBubblingEvents = toLookup([
   'abort',
@@ -22,11 +23,9 @@ const nonBubblingEvents = toLookup([
   'DOMNodeRemovedFromDocument',
 ]);
 
-const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']);
+const alwaysPreventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
 
-export interface OnEventCallback {
-  (event: Event, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>, eventFieldInfo: EventFieldInfo | null): void;
-}
+const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']);
 
 // 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
@@ -40,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));
@@ -105,41 +104,72 @@ 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 eventArgs: EventForDotNet<UIEventArgs> | null = null; // Populate lazily
-    const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
+    let candidateElement = browserEvent.target as Element | null;
+    let eventArgs: any = null; // Populate lazily
+    let eventArgsIsPopulated = false;
+    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 (!eventArgs) {
-            eventArgs = EventForDotNet.fromDOMEvent(evt);
+          if (!eventArgsIsPopulated) {
+            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;
           }
 
-          const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
-          this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs, eventFieldInfo);
+          // For certain built-in events, having any .NET handler implicitly means we will prevent
+          // 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: 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 {
@@ -161,6 +191,7 @@ class EventInfoStore {
   private countByEventName: { [eventName: string]: number } = {};
 
   constructor(private globalListener: EventListener) {
+    eventNameAliasRegisteredCallbacks.push(this.handleEventNameAliasAdded.bind(this));
   }
 
   public add(info: EventHandlerInfo) {
@@ -179,6 +210,9 @@ class EventInfoStore {
   }
 
   public addGlobalListener(eventName: string) {
+    // If this event name is an alias, update the global listener for the corresponding browser event
+    eventName = getBrowserEventName(eventName);
+
     if (this.countByEventName.hasOwnProperty(eventName)) {
       this.countByEventName[eventName]++;
     } else {
@@ -209,7 +243,9 @@ class EventInfoStore {
     if (info) {
       delete this.infosByEventHandlerId[eventHandlerId];
 
-      const eventName = info.eventName;
+      // If this event name is an alias, update the global listener for the corresponding browser event
+      const eventName = getBrowserEventName(info.eventName);
+
       if (--this.countByEventName[eventName] === 0) {
         delete this.countByEventName[eventName];
         document.removeEventListener(eventName, this.globalListener);
@@ -218,6 +254,22 @@ class EventInfoStore {
 
     return info;
   }
+
+  private handleEventNameAliasAdded(aliasEventName, browserEventName) {
+    // If an event name alias gets registered later, we need to update the global listener
+    // registrations to match. This makes it equivalent to the alias having been registered
+    // before the elements with event handlers got rendered.
+    if (this.countByEventName.hasOwnProperty(aliasEventName)) {
+      // Delete old
+      const countByAliasEventName = this.countByEventName[aliasEventName];
+      delete this.countByEventName[aliasEventName];
+      document.removeEventListener(aliasEventName, this.globalListener);
+
+      // Ensure corresponding count is added to new
+      this.addGlobalListener(browserEventName);
+      this.countByEventName[browserEventName] += countByAliasEventName - 1;
+    }
+  }
 }
 
 class EventHandlerInfosForElement {
@@ -281,10 +333,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;
 }

+ 11 - 5
src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts → src/Components/Web.JS/src/Rendering/Events/EventDispatcher.ts

@@ -1,11 +1,17 @@
-import { EventDescriptor } from './BrowserRenderer';
-import { UIEventArgs } from './EventForDotNet';
+import { EventFieldInfo } from './EventFieldInfo';
 
-type EventDispatcher = (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => void;
+export interface EventDescriptor {
+  browserRendererId: number;
+  eventHandlerId: number;
+  eventName: string;
+  eventFieldInfo: EventFieldInfo | null;
+}
+
+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 +19,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;
 }

+ 0 - 0
src/Components/Web.JS/src/Rendering/EventFieldInfo.ts → src/Components/Web.JS/src/Rendering/Events/EventFieldInfo.ts


+ 377 - 0
src/Components/Web.JS/src/Rendering/Events/EventTypes.ts

@@ -0,0 +1,377 @@
+interface EventTypeOptions {
+  browserEventName?: string;
+  createEventArgs?: (event: Event) => any;
+}
+
+const eventTypeRegistry: Map<string, EventTypeOptions> = new Map();
+const browserEventNamesToAliases: Map<string, string[]> = new Map();
+const createBlankEventArgsOptions: EventTypeOptions = { createEventArgs: () => ({}) };
+
+export const eventNameAliasRegisteredCallbacks: ((aliasEventName: string, browserEventName) => void)[] = [];
+
+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.`);
+  }
+
+  // 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]);
+    }
+
+    // For developer convenience, it's allowed to register the custom event type *after*
+    // some listeners for it are already present. Once the event name alias gets registered,
+    // we have to notify any existing event delegators so they can update their delegated
+    // events list.
+    eventNameAliasRegisteredCallbacks.forEach(callback => callback(eventName, options.browserEventName));
+  }
+
+  eventTypeRegistry.set(eventName, options);
+}
+
+export function getEventTypeOptions(eventName: string): EventTypeOptions | undefined {
+  return eventTypeRegistry.get(eventName);
+}
+
+export function getEventNameAliases(eventName: string): string[] | undefined {
+  return browserEventNamesToAliases.get(eventName);
+}
+
+export function getBrowserEventName(possibleAliasEventName: string): string {
+  const eventOptions = eventTypeRegistry.get(possibleAliasEventName);
+  return eventOptions?.browserEventName || possibleAliasEventName;
+}
+
+function registerBuiltInEventType(eventNames: string[], options: EventTypeOptions) {
+  eventNames.forEach(eventName => eventTypeRegistry.set(eventName, options));
+}
+
+registerBuiltInEventType(['input', 'change'], {
+  createEventArgs: parseChangeEvent
+});
+
+registerBuiltInEventType(['copy', 'cut', 'paste'], createBlankEventArgsOptions);
+
+registerBuiltInEventType(['drag', 'dragend', 'dragenter', 'dragleave', 'dragover', 'dragstart', 'drop'], {
+  createEventArgs: e => parseDragEvent(e as DragEvent)
+});
+
+registerBuiltInEventType(['focus', 'blur', 'focusin', 'focusout'], createBlankEventArgsOptions);
+
+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'], createBlankEventArgsOptions);
+
+function parseChangeEvent(event: Event): ChangeEventArgs {
+  const element = event.target as Element;
+  if (isTimeBasedInput(element)) {
+    const normalizedValue = normalizeTimeBasedValue(element);
+    return { value: normalizedValue };
+  } else {
+    const targetIsCheckbox = isCheckbox(element);
+    const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
+    return { value: newValue };
+  }
+}
+
+function parseWheelEvent(event: WheelEvent): WheelEventArgs {
+  return {
+    ...parseMouseEvent(event),
+    deltaX: event.deltaX,
+    deltaY: event.deltaY,
+    deltaZ: event.deltaZ,
+    deltaMode: event.deltaMode,
+  };
+}
+
+function parsePointerEvent(event: PointerEvent): PointerEventArgs {
+  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,
+  };
+}
+
+function parseTouchEvent(event: TouchEvent): TouchEventArgs {
+  return {
+    detail: event.detail,
+    touches: parseTouch(event.touches),
+    targetTouches: parseTouch(event.targetTouches),
+    changedTouches: parseTouch(event.changedTouches),
+    ctrlKey: event.ctrlKey,
+    shiftKey: event.shiftKey,
+    altKey: event.altKey,
+    metaKey: event.metaKey,
+  };
+}
+
+function parseProgressEvent(event: ProgressEvent<EventTarget>): 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,
+    code: event.code,
+    location: event.location,
+    repeat: event.repeat,
+    ctrlKey: event.ctrlKey,
+    shiftKey: event.shiftKey,
+    altKey: event.altKey,
+    metaKey: event.metaKey,
+  };
+}
+
+function parseDragEvent(event: DragEvent): DragEventArgs {
+  return {
+    ...parseMouseEvent(event),
+    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,
+    screenX: event.screenX,
+    screenY: event.screenY,
+    clientX: event.clientX,
+    clientY: event.clientY,
+    offsetX: event.offsetX,
+    offsetY: event.offsetY,
+    button: event.button,
+    buttons: event.buttons,
+    ctrlKey: event.ctrlKey,
+    shiftKey: event.shiftKey,
+    altKey: event.altKey,
+    metaKey: event.metaKey,
+  };
+}
+
+function isCheckbox(element: Element | null): boolean {
+  return !!element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
+}
+
+const timeBasedInputs = [
+  'date',
+  'datetime-local',
+  'month',
+  'time',
+  'week',
+];
+
+function isTimeBasedInput(element: Element): element is HTMLInputElement {
+  return timeBasedInputs.indexOf(element.getAttribute('type')!) !== -1;
+}
+
+function normalizeTimeBasedValue(element: HTMLInputElement): string {
+  const value = element.value;
+  const type = element.type;
+  switch (type) {
+    case 'date':
+    case 'datetime-local':
+    case 'month':
+      return value;
+    case 'time':
+      return value.length === 5 ? value + ':00' : value; // Convert hh:mm to hh:mm:00
+    case 'week':
+      // For now we are not going to normalize input type week as it is not trivial
+      return value;
+  }
+
+  throw new Error(`Invalid element type '${type}'.`);
+}
+
+// The following interfaces must be kept in sync with the EventArgs C# classes
+
+interface ChangeEventArgs {
+  value: string | boolean;
+}
+
+interface DragEventArgs {
+  detail: number;
+  dataTransfer: DataTransferEventArgs | null;
+  screenX: number;
+  screenY: number;
+  clientX: number;
+  clientY: number;
+  button: number;
+  buttons: number;
+  ctrlKey: boolean;
+  shiftKey: boolean;
+  altKey: boolean;
+  metaKey: boolean;
+}
+
+interface DataTransferEventArgs {
+  dropEffect: string;
+  effectAllowed: string;
+  files: readonly string[];
+  items: readonly DataTransferItem[];
+  types: readonly string[];
+}
+
+interface DataTransferItem {
+  kind: string;
+  type: string;
+}
+
+interface ErrorEventArgs {
+  message: string;
+  filename: string;
+  lineno: number;
+  colno: number;
+
+  // omitting 'error' here since we'd have to serialize it, and it's not clear we will want to
+  // do that. https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
+}
+
+interface KeyboardEventArgs {
+  key: string;
+  code: string;
+  location: number;
+  repeat: boolean;
+  ctrlKey: boolean;
+  shiftKey: boolean;
+  altKey: boolean;
+  metaKey: boolean;
+}
+
+interface MouseEventArgs {
+  detail: number;
+  screenX: number;
+  screenY: number;
+  clientX: number;
+  clientY: number;
+  offsetX: number;
+  offsetY: number;
+  button: number;
+  buttons: number;
+  ctrlKey: boolean;
+  shiftKey: boolean;
+  altKey: boolean;
+  metaKey: boolean;
+}
+
+interface PointerEventArgs extends MouseEventArgs {
+  pointerId: number;
+  width: number;
+  height: number;
+  pressure: number;
+  tiltX: number;
+  tiltY: number;
+  pointerType: string;
+  isPrimary: boolean;
+}
+
+interface ProgressEventArgs {
+  lengthComputable: boolean;
+  loaded: number;
+  total: number;
+}
+
+interface TouchEventArgs {
+  detail: number;
+  touches: TouchPoint[];
+  targetTouches: TouchPoint[];
+  changedTouches: TouchPoint[];
+  ctrlKey: boolean;
+  shiftKey: boolean;
+  altKey: boolean;
+  metaKey: boolean;
+}
+
+interface TouchPoint {
+  identifier: number;
+  screenX: number;
+  screenY: number;
+  clientX: number;
+  clientY: number;
+  pageX: number;
+  pageY: number;
+}
+
+interface WheelEventArgs extends MouseEventArgs {
+  deltaX: number;
+  deltaY: number;
+  deltaZ: number;
+  deltaMode: number;
+}

+ 1 - 1
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;

+ 4 - 0
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<Microsoft.AspNetCore.Components.Forms.IBrowserFile!>
 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.Forms.IBrowserFile!>
+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

+ 20 - 0
src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs

@@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<ClipboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<ClipboardEventArgs> callback)
         {
             if (factory == null)
@@ -37,6 +38,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<ClipboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<ClipboardEventArgs, Task> callback)
         {
             if (factory == null)
@@ -55,6 +57,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<DragEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<DragEventArgs> callback)
         {
             if (factory == null)
@@ -73,6 +76,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<DragEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<DragEventArgs, Task> callback)
         {
             if (factory == null)
@@ -91,6 +95,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<ErrorEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<ErrorEventArgs> callback)
         {
             if (factory == null)
@@ -109,6 +114,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<ErrorEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<ErrorEventArgs, Task> callback)
         {
             if (factory == null)
@@ -127,6 +133,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<FocusEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<FocusEventArgs> callback)
         {
             if (factory == null)
@@ -145,6 +152,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<FocusEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<FocusEventArgs, Task> callback)
         {
             if (factory == null)
@@ -163,6 +171,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<KeyboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<KeyboardEventArgs> callback)
         {
             if (factory == null)
@@ -181,6 +190,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<KeyboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<KeyboardEventArgs, Task> callback)
         {
             if (factory == null)
@@ -199,6 +209,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<MouseEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<MouseEventArgs> callback)
         {
             if (factory == null)
@@ -217,6 +228,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<MouseEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<MouseEventArgs, Task> callback)
         {
             if (factory == null)
@@ -234,6 +246,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<PointerEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<PointerEventArgs> callback)
         {
             if (factory == null)
@@ -252,6 +265,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<PointerEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<PointerEventArgs, Task> callback)
         {
             if (factory == null)
@@ -270,6 +284,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<ProgressEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<ProgressEventArgs> callback)
         {
             if (factory == null)
@@ -288,6 +303,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<ProgressEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<ProgressEventArgs, Task> callback)
         {
             if (factory == null)
@@ -306,6 +322,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<TouchEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<TouchEventArgs> callback)
         {
             if (factory == null)
@@ -324,6 +341,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<TouchEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<TouchEventArgs, Task> callback)
         {
             if (factory == null)
@@ -342,6 +360,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<WheelEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<WheelEventArgs> callback)
         {
             if (factory == null)
@@ -360,6 +379,7 @@ namespace Microsoft.AspNetCore.Components.Web
         /// <param name="receiver">The event receiver.</param>
         /// <param name="callback">The event callback.</param>
         /// <returns>The <see cref="EventCallback"/>.</returns>
+        [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")]
         public static EventCallback<WheelEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<WheelEventArgs, Task> callback)
         {
             if (factory == null)

+ 1 - 1
src/Components/Web/src/WebEventDescriptor.cs

@@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
         /// <summary>
         /// For framework use only.
         /// </summary>
-        public string EventArgsType { get; set; } = default!;
+        public string EventName { get; set; } = default!;
 
         /// <summary>
         /// For framework use only.

+ 1 - 1
src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs

@@ -33,8 +33,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Infrastructure
         [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,

+ 2 - 2
src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs

@@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
             {
                 BrowserRendererId = 0,
                 EventHandlerId = 3,
-                EventArgsType = "mouse",
+                EventName = "click",
             });
 
             // Act
@@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
             {
                 BrowserRendererId = 0,
                 EventHandlerId = 1990,
-                EventArgsType = "mouse",
+                EventName = "click",
             });
 
             var eventArgs = new MouseEventArgs

+ 2 - 2
src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs

@@ -440,7 +440,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
             {
                 BrowserRendererId = 0,
                 EventHandlerId = 6,
-                EventArgsType = "mouse",
+                EventName = "click",
             };
 
             await Client.ExpectCircuitError(async () =>
@@ -478,7 +478,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
             {
                 BrowserRendererId = 0,
                 EventHandlerId = 1,
-                EventArgsType = "mouse",
+                EventName = "click",
             };
 
             await Client.ExpectCircuitError(async () =>

+ 8 - 0
src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs

@@ -99,4 +99,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
         {
         }
     }
+
+    public class ServerEventCustomArgsTest : EventCustomArgsTest
+    {
+        public ServerEventCustomArgsTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
+            : base(browserFixture, serverFixture.WithServerExecution(), output)
+        {
+        }
+    }
 }

+ 179 - 0
src/Components/test/E2ETest/Tests/EventCustomArgsTest.cs

@@ -0,0 +1,179 @@
+// 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.Linq;
+using BasicTestApp;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
+using Microsoft.AspNetCore.E2ETesting;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Interactions;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.AspNetCore.Components.E2ETest.Tests
+{
+    public class EventCustomArgsTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
+    {
+        public EventCustomArgsTest(
+            BrowserFixture browserFixture,
+            ToggleExecutionModeServerFixture<Program> serverFixture,
+            ITestOutputHelper output)
+            : base(browserFixture, serverFixture, output)
+        {
+        }
+
+        protected override void InitializeAsyncCore()
+        {
+            // Always do a full page reload because these tests need to start with no custom event registrations
+            Navigate(ServerPathBase, noReload: false);
+            Browser.MountTestComponent<EventCustomArgsComponent>();
+        }
+
+        [Fact]
+        public void UnregisteredCustomEventWorks()
+        {
+            // This reflects functionality in 5.0 and earlier, in which you could have custom events
+            // registered with the Razor compiler but no way to register them with the runtime, so
+            // you could only receive empty eventargs.
+            Browser.Exists(By.Id("trigger-testevent-directly")).Click();
+            Browser.Equal("Received testevent with args '{ MyProp=null }'", () => GetLogLines().Single());
+        }
+
+        [Fact]
+        public void CanRegisterCustomEventAfterRender_WithNoCreateEventArgs()
+        {
+            Browser.Exists(By.Id("register-testevent-with-no-createventargs")).Click();
+            Browser.FindElement(By.Id("trigger-testevent-directly")).Click();
+            Browser.Equal("Received testevent with args '{ MyProp=null }'", () => GetLogLines().Single());
+        }
+
+        [Fact]
+        public void CanRegisterCustomEventAfterRender_WithCreateEventArgsReturningNull()
+        {
+            Browser.Exists(By.Id("register-testevent-with-createventargs-that-returns-null")).Click();
+            Browser.FindElement(By.Id("trigger-testevent-directly")).Click();
+            Browser.Equal("Received testevent with args 'null'", () => GetLogLines().Single());
+        }
+
+        [Fact]
+        public void CanRegisterCustomEventAfterRender_WithCreateEventArgsReturningData()
+        {
+            Browser.Exists(By.Id("register-testevent-with-createventargs-that-supplies-args")).Click();
+            Browser.FindElement(By.Id("trigger-testevent-directly")).Click();
+            Browser.Equal("Received testevent with args '{ MyProp=Native event target ID=test-event-target-child }'", () => GetLogLines().Single());
+        }
+
+        [Fact]
+        public void CanAliasBrowserEvent_WithCreateEventArgsReturningData()
+        {
+            var input = Browser.Exists(By.CssSelector("#test-event-target-child input"));
+            Browser.FindElement(By.Id("register-custom-keydown")).Click();
+            SendKeysSequentially(input, "ab");
+
+            Browser.Equal(new[]
+            {
+                "Received native keydown event",
+                "You pressed: a",
+                "Received native keydown event",
+                "You pressed: b",
+            }, GetLogLines);
+
+            Assert.Equal("ab", input.GetAttribute("value"));
+        }
+
+        [Fact]
+        public void CanAliasBrowserEvent_PreventDefaultOnNativeEvent()
+        {
+            var input = Browser.Exists(By.CssSelector("#test-event-target-child input"));
+            Browser.FindElement(By.Id("register-custom-keydown")).Click();
+            Browser.FindElement(By.Id("custom-keydown-prevent-default")).Click();
+            SendKeysSequentially(input, "ab");
+
+            Browser.Equal(new[]
+            {
+                "Received native keydown event",
+                "You pressed: a",
+                "Received native keydown event",
+                "You pressed: b",
+            }, GetLogLines);
+
+            // Check it was actually preventDefault-ed
+            Assert.Equal("", input.GetAttribute("value"));
+        }
+
+        [Fact]
+        public void CanAliasBrowserEvent_StopPropagationIndependentOfNativeEvent()
+        {
+            var input = Browser.Exists(By.CssSelector("#test-event-target-child input"));
+            Browser.FindElement(By.Id("register-custom-keydown")).Click();
+            Browser.FindElement(By.Id("register-yet-another-keydown")).Click();
+            Browser.FindElement(By.Id("custom-keydown-stop-propagation")).Click();
+            SendKeysSequentially(input, "ab");
+
+            Browser.Equal(new[]
+            {
+                // The native event still bubbles up to its listener on an ancestor, and
+                // other aliased events still receive it, but the stopPropagation-ed
+                // variant does not
+                "Received native keydown event",
+                "Yet another aliased event received: a",
+                "Received native keydown event",
+                "Yet another aliased event received: b",
+            }, GetLogLines);
+
+            Assert.Equal("ab", input.GetAttribute("value"));
+        }
+
+        [Fact]
+        public void CanHaveMultipleAliasesForASingleBrowserEvent()
+        {
+            var input = Browser.Exists(By.CssSelector("#test-event-target-child input"));
+            Browser.FindElement(By.Id("register-custom-keydown")).Click();
+            Browser.FindElement(By.Id("register-yet-another-keydown")).Click();
+            SendKeysSequentially(input, "ab");
+
+            Browser.Equal(new[]
+            {
+                "Received native keydown event",
+                "You pressed: a",
+                "Yet another aliased event received: a",
+                "Received native keydown event",
+                "You pressed: b",
+                "Yet another aliased event received: b",
+            }, GetLogLines);
+
+            Assert.Equal("ab", input.GetAttribute("value"));
+        }
+
+        [Fact]
+        public void CanAliasBrowserEvent_WithoutAnyNativeListenerForBrowserEvent()
+        {
+            // Sets up a registration for a custom event name that's an alias for mouseover,
+            // but there's no regular listener for mouseover in the application at this point
+            Browser.Exists(By.Id("register-custom-mouseover")).Click();
+
+            new Actions(Browser)
+                .MoveToElement(Browser.FindElement(By.Id("test-event-target-child")))
+                .Perform();
+
+            // Nonetheless, the custom event is still received
+            Browser.True(() => GetLogLines().Contains("Received custom mouseover event"));
+        }
+
+        void SendKeysSequentially(IWebElement target, string text)
+        {
+            foreach (var c in text)
+            {
+                target.SendKeys(c.ToString());
+            }
+        }
+
+        private string[] GetLogLines()
+            => Browser.Exists(By.Id("test-log"))
+            .GetAttribute("value")
+            .Replace("\r\n", "\n")
+            .Split('\n', StringSplitOptions.RemoveEmptyEntries);
+    }
+}

+ 25 - 1
src/Components/test/E2ETest/Tests/EventTest.cs

@@ -26,7 +26,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
         protected override void InitializeAsyncCore()
         {
             Navigate(ServerPathBase, noReload: true);
-            Browser.MountTestComponent<EventBubblingComponent>();
         }
 
         [Fact]
@@ -300,6 +299,31 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
             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<MouseEventComponent>();
+
+            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

+ 6 - 7
src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor

@@ -1,10 +1,9 @@
 <h3 id="event-bubbling">Bubbling standard event</h3>
 
-@* Temporarily hard-coding the internal names - this will be replaced once the Razor compiler supports @onevent:stopPropagation and @onevent:preventDefault *@
 <div @onclick="@(() => LogEvent("parent onclick"))">
     @* This element shows you can stop propagation even without necessarily also handling the event *@
-    <div __internal_stopPropagation_onclick="@intermediateStopPropagation">
-        <button id="button-with-onclick" @onclick="@(() => LogEvent("target onclick"))" __internal_stopPropagation_onclick="@targetStopPropagation">
+    <div @onclick:stopPropagation="@intermediateStopPropagation">
+        <button id="button-with-onclick" @onclick="@(() => LogEvent("target onclick"))" @onclick:stopPropagation="@targetStopPropagation">
             Button with onclick handler
         </button>
         <button id="button-without-onclick">
@@ -27,21 +26,21 @@
 
 <h3>PreventDefault</h3>
 
-<div __internal_preventDefault_onclick="@ancestorPreventDefault">
+<div @onclick:preventDefault="@ancestorPreventDefault">
     <p>
         <label>
-            <input type="checkbox" id="checkbox-with-preventDefault-true" __internal_preventDefault_onclick @onclick="@(() => LogEvent("Checkbox click"))" @onchange="@(() => LogEvent("Checkbox change"))" />
+            <input type="checkbox" id="checkbox-with-preventDefault-true" @onclick:preventDefault @onclick="@(() => LogEvent("Checkbox click"))" @onchange="@(() => LogEvent("Checkbox change"))" />
             Checkbox with onclick preventDefault
         </label>
     </p>
     <p>
         <label>
-            <input type="checkbox" id="checkbox-with-preventDefault-false" __internal_preventDefault_onclick="@false" @onclick="@(() => LogEvent("Checkbox click"))" @onchange="@(() => LogEvent("Checkbox change"))" />
+            <input type="checkbox" id="checkbox-with-preventDefault-false" @onclick:preventDefault="@false" @onclick="@(() => LogEvent("Checkbox click"))" @onchange="@(() => LogEvent("Checkbox change"))" />
             Checkbox with onclick preventDefault = false
         </label>
     </p>
     <p>
-        Textbox that can block keystrokes: <input id="textbox-that-can-block-keystrokes" __internal_preventDefault_onkeydown="@preventOnKeyDown" @onkeydown="@(() => LogEvent("Received keydown"))" />
+        Textbox that can block keystrokes: <input id="textbox-that-can-block-keystrokes" @onkeydown:preventDefault="@preventOnKeyDown" @onkeydown="@(() => LogEvent("Received keydown"))" />
     </p>
 </div>
 

+ 102 - 0
src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor

@@ -0,0 +1,102 @@
+@using BasicTestApp.CustomEventTypesNamespace
+
+<h3 id="event-custom-args">Custom event types</h3>
+
+<p>This component exercises various scenarios around custom event types and arguments.</p>
+
+<div id="test-event-target" style="border: 1px solid black; background: silver; padding: 1em; margin-bottom: 1em;"
+     @onkeydown="@(e => { LogMessage("Received native keydown event"); })"
+     @ontestevent="@HandleTestEvent"
+     @onkeydown.testvariant="@HandleCustomKeyDown"
+     @onkeydown.yetanother="@HandleYetAnotherKeyboardEvent"
+     @oncustommouseover="@(e => { LogMessage("Received custom mouseover event"); })">
+    Event target
+    <div id="test-event-target-child" style="background: #afa; padding: 1em;">
+        Child
+        <p @onkeydown.testvariant:preventDefault="@customKeyDownPreventDefault"
+           @onkeydown.testvariant:stopPropagation="@customKeyDownStopPropagation">
+            <input placeholder="Type into me" />
+        </p>
+    </div>
+</div>
+
+<button id="trigger-testevent-directly"
+        onclick="document.getElementById('test-event-target-child').dispatchEvent(new CustomEvent('testevent', { bubbles: true }))">
+    Trigger testevent directly
+</button>
+
+<button id="register-testevent-with-no-createventargs"
+        onclick="Blazor.registerCustomEventType('testevent', {})">
+    Register testevent with no createventargs
+</button>
+
+<button id="register-testevent-with-createventargs-that-returns-null"
+        onclick="Blazor.registerCustomEventType('testevent', { createEventArgs: e => null })">
+    Register testevent with createventargs that returns null
+</button>
+
+<button id="register-testevent-with-createventargs-that-supplies-args"
+        onclick="Blazor.registerCustomEventType('testevent', { createEventArgs: e => ({ myProp: `Native event target ID=${e.target.id}` }) })">
+    Register testevent with createventargs that supplies args
+</button>
+
+<button id="register-custom-keydown"
+        onclick="Blazor.registerCustomEventType('keydown.testvariant', { browserEventName: 'keydown', createEventArgs: event => ({ customKeyInfo: event.key }) })">
+    Register custom keydown event
+</button>
+
+<button id="register-yet-another-keydown"
+        onclick="Blazor.registerCustomEventType('keydown.yetanother', { browserEventName: 'keydown', createEventArgs: event => ({ youPressed: event.key }) })">
+    Register yet another custom keyboard event
+</button>
+
+<button id="register-custom-mouseover"
+        onclick="Blazor.registerCustomEventType('custommouseover', { browserEventName: 'mouseover' })">
+    Register custom mouseover event (which has no corresponding native listener)
+</button>
+
+<p>
+    <label>
+        <input type="checkbox" id="custom-keydown-prevent-default" @bind="customKeyDownPreventDefault" />
+        Custom keydown: prevent default
+    </label>
+</p>
+
+<p>
+    <label>
+        <input type="checkbox" id="custom-keydown-stop-propagation" @bind="customKeyDownStopPropagation" />
+        Custom keydown: stop propagation
+    </label>
+</p>
+
+<h3>Event log</h3>
+
+<textarea id="test-log" readonly @bind="logValue" cols="100" rows="10"></textarea>
+<button id="clear-log" @onclick="@(() => { logValue = string.Empty; })">Clear log</button>
+
+@code {
+    string logValue = string.Empty;
+    bool customKeyDownPreventDefault;
+    bool customKeyDownStopPropagation;
+
+    void LogMessage(string message)
+    {
+        logValue += message + Environment.NewLine;
+    }
+
+    void HandleTestEvent(TestEventArgs eventArgs)
+    {
+        var args = eventArgs == null ? "null" : $"{{ MyProp={eventArgs.MyProp ?? "null"} }}";
+        LogMessage($"Received testevent with args '{args}'");
+    }
+
+    void HandleCustomKeyDown(TestKeyDownEventArgs eventArgs)
+    {
+        LogMessage($"You pressed: {eventArgs.CustomKeyInfo}");
+    }
+
+    void HandleYetAnotherKeyboardEvent(YetAnotherCustomKeyboardEventArgs eventArgs)
+    {
+        LogMessage($"Yet another aliased event received: {eventArgs.YouPressed}");
+    }
+}

+ 28 - 0
src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs

@@ -0,0 +1,28 @@
+using System;
+using Microsoft.AspNetCore.Components;
+
+namespace BasicTestApp.CustomEventTypesNamespace
+{
+    [EventHandler("ontestevent", typeof(TestEventArgs), true, true)]
+    [EventHandler("onkeydown.testvariant", typeof(TestKeyDownEventArgs), true, true)]
+    [EventHandler("onkeydown.yetanother", typeof(YetAnotherCustomKeyboardEventArgs), true, true)]
+    [EventHandler("oncustommouseover", typeof(EventArgs), true, true)]
+    public static class EventHandlers
+    {
+    }
+
+    class TestEventArgs : EventArgs
+    {
+        public string MyProp { get; set; }
+    }
+
+    class TestKeyDownEventArgs : EventArgs
+    {
+        public string CustomKeyInfo { get; set; }
+    }
+
+    class YetAnotherCustomKeyboardEventArgs : EventArgs
+    {
+        public string YouPressed { get; set; }
+    }
+}

+ 0 - 4
src/Components/test/testassets/BasicTestApp/EventPreventDefaultComponent.razor

@@ -6,10 +6,6 @@
     you almost certainly don't really want to perform a server-side post, especially given that
     it would occur <em>before</em> an async event handler.
 </p>
-<p>
-    Later, it's likely that we'll add a syntax for controlling whether any given event handler
-    triggers a synchronous <code>preventDefault</code> before the event handler runs.
-</p>
 
 <h2>Form with onsubmit handler</h2>
 

+ 1 - 1
src/Components/test/testassets/BasicTestApp/Index.razor

@@ -27,7 +27,7 @@
         <option value="BasicTestApp.ErrorComponent">Error throwing</option>
         <option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
         <option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
-        <option value="BasicTestApp.EventCasesComponent">Event cases</option>
+        <option value="BasicTestApp.EventCustomArgsComponent">Event custom arguments</option>
         <option value="BasicTestApp.EventDisablingComponent">Event disabling</option>
         <option value="BasicTestApp.EventDuringBatchRendering">Event during batch rendering</option>
         <option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>

+ 26 - 9
src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor

@@ -23,12 +23,17 @@
         <div id="drop" @ondrop="OnDrop" ondragover="event.preventDefault()" style="width: 100px; height: 100px; border: dotted">Drop Target</div>
     </p>
     <p>
-        <button @onclick="Clear">Clear</button>
+        <button id="clear_event_log" @onclick="Clear">Clear</button>
     </p>
 
     <p>
         Another input (to distract you) <input id="other" />
     </p>
+
+    <p>
+        Polymorphic args handler:
+        <div id="polymorphic_event_elem" draggable="true" @onpointerup="OnPolymorphicEvent" @ondragstart="OnPolymorphicEvent">Click or drag me</div>
+    </p>
 </div>
 
 @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)

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません