Skip to content

Commit d97be90

Browse files
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
1 parent 0e3cb14 commit d97be90

32 files changed

+1192
-514
lines changed

src/Components/Components/src/PublicAPI.Unshipped.txt

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Microsoft.AspNetCore.Components.DynamicComponent.Type.set -> void
1515
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute
1616
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.CascadingTypeParameterAttribute(string! name) -> void
1717
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.Name.get -> string!
18+
Microsoft.AspNetCore.Components.RenderTree.Renderer.GetEventArgsType(ulong eventHandlerId) -> System.Type!
1819
static Microsoft.AspNetCore.Components.ParameterView.FromDictionary(System.Collections.Generic.IDictionary<string!, object?>! parameters) -> Microsoft.AspNetCore.Components.ParameterView
1920
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs) -> System.Threading.Tasks.Task!
2021
*REMOVED*readonly Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit.RemovedAttributeName -> string
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Reflection;
7+
8+
namespace Microsoft.AspNetCore.Components.RenderTree
9+
{
10+
internal static class EventArgsTypeCache
11+
{
12+
private static ConcurrentDictionary<MethodInfo, Type> Cache = new ConcurrentDictionary<MethodInfo, Type>();
13+
14+
public static Type GetEventArgsType(MethodInfo methodInfo)
15+
{
16+
return Cache.GetOrAdd(methodInfo, methodInfo =>
17+
{
18+
var parameterInfos = methodInfo.GetParameters();
19+
if (parameterInfos.Length == 0)
20+
{
21+
return typeof(EventArgs);
22+
}
23+
else if (parameterInfos.Length > 1)
24+
{
25+
throw new InvalidOperationException($"The method {methodInfo} cannot be used as an event handler because it declares more than one parameter.");
26+
}
27+
else
28+
{
29+
var declaredType = parameterInfos[0].ParameterType;
30+
if (typeof(EventArgs).IsAssignableFrom(declaredType))
31+
{
32+
return declaredType;
33+
}
34+
else
35+
{
36+
throw new InvalidOperationException($"The event handler parameter type {declaredType.FullName} for event must inherit from {typeof(EventArgs).FullName}.");
37+
}
38+
}
39+
});
40+
}
41+
}
42+
}

src/Components/Components/src/RenderTree/Renderer.cs

+29-5
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,7 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
249249
{
250250
Dispatcher.AssertAccess();
251251

252-
if (!_eventBindings.TryGetValue(eventHandlerId, out var callback))
253-
{
254-
throw new ArgumentException($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", nameof(eventHandlerId));
255-
}
256-
252+
var callback = GetRequiredEventCallback(eventHandlerId);
257253
Log.HandlingEvent(_logger, eventHandlerId, eventArgs);
258254

259255
if (fieldInfo != null)
@@ -291,6 +287,24 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
291287
return result;
292288
}
293289

290+
/// <summary>
291+
/// Gets the event arguments type for the specified event handler.
292+
/// </summary>
293+
/// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
294+
/// <returns>The parameter type expected by the event handler. Normally this is a subclass of <see cref="EventArgs"/>.</returns>
295+
public Type GetEventArgsType(ulong eventHandlerId)
296+
{
297+
var methodInfo = GetRequiredEventCallback(eventHandlerId).Delegate?.Method;
298+
299+
// The DispatchEventAsync code paths allow for the case where Delegate or its method
300+
// is null, and in this case the event receiver just receives null. This won't happen
301+
// under normal circumstances, but to avoid creating a new failure scenario, allow for
302+
// that edge case here too.
303+
return methodInfo == null
304+
? typeof(EventArgs)
305+
: EventArgsTypeCache.GetEventArgsType(methodInfo);
306+
}
307+
294308
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
295309
{
296310
if (frame.FrameTypeField != RenderTreeFrameType.Component)
@@ -404,6 +418,16 @@ internal void TrackReplacedEventHandlerId(ulong oldEventHandlerId, ulong newEven
404418
_eventHandlerIdReplacements.Add(oldEventHandlerId, newEventHandlerId);
405419
}
406420

421+
private EventCallback GetRequiredEventCallback(ulong eventHandlerId)
422+
{
423+
if (!_eventBindings.TryGetValue(eventHandlerId, out var callback))
424+
{
425+
throw new ArgumentException($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", nameof(eventHandlerId));
426+
}
427+
428+
return callback;
429+
}
430+
407431
private ulong FindLatestEventHandlerIdInChain(ulong eventHandlerId)
408432
{
409433
while (_eventHandlerIdReplacements.TryGetValue(eventHandlerId, out var replacementEventHandlerId))

src/Components/Components/test/RendererTest.cs

+101
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,98 @@ public void CanDispatchEventsToTopLevelComponents()
484484
Assert.Same(eventArgs, receivedArgs);
485485
}
486486

487+
[Fact]
488+
public void CanGetEventArgsTypeForHandler()
489+
{
490+
// Arrange: Render a component with an event handler
491+
var renderer = new TestRenderer();
492+
493+
var component = new EventComponent
494+
{
495+
OnArbitraryDelegateEvent = (Func<DerivedEventArgs, Task>)(args => Task.CompletedTask),
496+
};
497+
var componentId = renderer.AssignRootComponentId(component);
498+
component.TriggerRender();
499+
500+
var eventHandlerId = renderer.Batches.Single()
501+
.ReferenceFrames
502+
.First(frame => frame.AttributeValue != null)
503+
.AttributeEventHandlerId;
504+
505+
// Assert: Can determine event args type
506+
var eventArgsType = renderer.GetEventArgsType(eventHandlerId);
507+
Assert.Same(typeof(DerivedEventArgs), eventArgsType);
508+
}
509+
510+
[Fact]
511+
public void CanGetEventArgsTypeForParameterlessHandler()
512+
{
513+
// Arrange: Render a component with an event handler
514+
var renderer = new TestRenderer();
515+
516+
var component = new EventComponent
517+
{
518+
OnArbitraryDelegateEvent = (Func<Task>)(() => Task.CompletedTask),
519+
};
520+
var componentId = renderer.AssignRootComponentId(component);
521+
component.TriggerRender();
522+
523+
var eventHandlerId = renderer.Batches.Single()
524+
.ReferenceFrames
525+
.First(frame => frame.AttributeValue != null)
526+
.AttributeEventHandlerId;
527+
528+
// Assert: Can determine event args type
529+
var eventArgsType = renderer.GetEventArgsType(eventHandlerId);
530+
Assert.Same(typeof(EventArgs), eventArgsType);
531+
}
532+
533+
[Fact]
534+
public void CannotGetEventArgsTypeForMultiParameterHandler()
535+
{
536+
// Arrange: Render a component with an event handler
537+
var renderer = new TestRenderer();
538+
539+
var component = new EventComponent
540+
{
541+
OnArbitraryDelegateEvent = (Action<EventArgs, string>)((x, y) => { }),
542+
};
543+
var componentId = renderer.AssignRootComponentId(component);
544+
component.TriggerRender();
545+
546+
var eventHandlerId = renderer.Batches.Single()
547+
.ReferenceFrames
548+
.First(frame => frame.AttributeValue != null)
549+
.AttributeEventHandlerId;
550+
551+
// Assert: Cannot determine event args type
552+
var ex = Assert.Throws<InvalidOperationException>(() => renderer.GetEventArgsType(eventHandlerId));
553+
Assert.Contains("declares more than one parameter", ex.Message);
554+
}
555+
556+
[Fact]
557+
public void CannotGetEventArgsTypeForHandlerWithNonEventArgsParameter()
558+
{
559+
// Arrange: Render a component with an event handler
560+
var renderer = new TestRenderer();
561+
562+
var component = new EventComponent
563+
{
564+
OnArbitraryDelegateEvent = (Action<DateTime>)(arg => { }),
565+
};
566+
var componentId = renderer.AssignRootComponentId(component);
567+
component.TriggerRender();
568+
569+
var eventHandlerId = renderer.Batches.Single()
570+
.ReferenceFrames
571+
.First(frame => frame.AttributeValue != null)
572+
.AttributeEventHandlerId;
573+
574+
// Assert: Cannot determine event args type
575+
var ex = Assert.Throws<InvalidOperationException>(() => renderer.GetEventArgsType(eventHandlerId));
576+
Assert.Contains($"must inherit from {typeof(EventArgs).FullName}", ex.Message);
577+
}
578+
487579
[Fact]
488580
public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers()
489581
{
@@ -4224,6 +4316,9 @@ private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
42244316
[Parameter]
42254317
public EventCallback<DerivedEventArgs> OnClickEventCallbackOfT { get; set; }
42264318

4319+
[Parameter]
4320+
public Delegate OnArbitraryDelegateEvent { get; set; }
4321+
42274322
public bool SkipElement { get; set; }
42284323
private int renderCount = 0;
42294324

@@ -4269,6 +4364,12 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
42694364
{
42704365
builder.AddAttribute(5, "onclickaction", OnClickAsyncAction);
42714366
}
4367+
4368+
if (OnArbitraryDelegateEvent != null)
4369+
{
4370+
builder.AddAttribute(6, "onarbitrarydelegateevent", OnArbitraryDelegateEvent);
4371+
}
4372+
42724373
builder.CloseElement();
42734374
builder.CloseElement();
42744375
}

src/Components/Server/src/Circuits/CircuitHost.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson
399399
WebEventData webEventData;
400400
try
401401
{
402-
webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson);
402+
webEventData = WebEventData.Parse(Renderer, eventDescriptorJson, eventArgsJson);
403403
}
404404
catch (Exception ex)
405405
{

0 commit comments

Comments
 (0)