-
Notifications
You must be signed in to change notification settings - Fork 10.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support custom event args in Blazor #17552
Comments
Would it be okay for you to add a description to the tickets, so it's users who follow them can understand what is being done? |
Is it possible to make some short explanation for this issue:
Since you are referring this issue as reason to close several other issues, it would help if we can see how this one is supposed to help. |
Thanks for contacting us. |
@dotnet/aspnet-blazor-eng I'm working on the design here, and have added the standard design proposal template above. If you have comments, please let me know! I think we've already had enough team discussions about this that the high-level goals are already pretty well agreed on, so I'm going to proceed with more implementation design. But if anyone on the team has concerns or differing opinions about the overall scope and direction (compared with the proposal above), please let me know as soon as you can. |
Hello Mr. Sanderson, I originally requested #27651 for custom web components, since we are using them. First of all thank you for this quick response. Two things I want to put in to consider for custom web components:
the alert will shown when the custom event is fired. LitElement implements this by checking if a attribute is called @ + event/CustomEvent name - here the CustomEvent. Then the value is run. I am not shure how it is run if it checks for a dollar sign or the javascript is interpreted. Since blazor also uses the @-Syntax this should maybe not result in the handler ending up in the html code, since that could interfere with the web component framework. I am shure LitElement and Blazor are not the only frameworks using (at) for custom elements (Angular? Vue? React?) Now I also asked in the LitElement Git Before I I found out this solution for LitElement Also I asked in the WICG Standardization if there should be a standard how to define event handlers for CustomEvents in html. So far the only standard is HtmlElement.addEventListener(); What is also to consider: CustomEvents always pass custom Parameters under .detail so myevent.detail.mytextvar or myevent.detail.myintvar. So maybe there could a CustomEventArgs(Of TDetail) class? Regarding the risks with EventHandler having unique names: Thank you for your time. |
Design work is done now. Ready for implementation. |
@mkArtakMSFT Moving this to preview 2 for the actual implementation work. |
Done in #29993 |
@SteveSandersonMS : This feature included in .NET v6.0.0-preview.1 release ? |
It'll be in 6.0-preview2, @pandiyarajm93. |
[Design proposal added by @SteveSandersonMS]
Summary
Currently Blazor allows developers to define custom events for their HTML elements. This works by using the
[EventHandler]
attribute to configure the Razor compiler to recognize a new event name (e.g.,@onmyevent=SomeHandler
). However, because of both implementation and design limitations, there is currently no way for these events to accept custom argument types, nor is it possible to change/extend the argument types accepted by predefined built-in events. The work here is to:Motivation and goals
We've had many customer requests for features that would be addressed by custom event arguments. Examples:
onpaste
native event #14133 - another request to get additional event data for a standard event, in this case thedata
from anonpaste
eventIn scope
onclick
) that supply additional custom data from JS to .NETOut of scope
Risks / unknowns
Currently, each
[EventHandler]
declaration's event name must be globally unique, or the Razor compiler throws. Should we loosen that restriction and have some kind of priority/overriding mechanism?onclick
to supply one set of data, and LibB wants it to supply different incompatible data? What if it's expensive to do LibA's thing - do we still do it all the time, or can we somehow know whether a given event handler is meant to do that? How do we know which 3rd-party library's definition to use in a given case?internal
types with[EventArgs]
should be ignored on referenced assemblies, so each 3rd-party lib can use its own internal event args definitions, but they don't pollute other projects that reference them - this is purely a compile-time thing anyway.)If we tried to auto-serialize all the data from a JS
Event
object to .NET just in case the custom handler wants to use it, that won't work when the data contains unserializable things or too much data. Seems inevitable the developer will have to supply JS-side logic for determining what data to pass to .NET.If we change aspects of how we know what types to pass to the .NET event handler methods, we might break back-compat in obscure cases. For example, when using
.razor
syntax the compiler always generates calls toEventCallback.Factory.Create<TEventArgs>
, so we know the resulting delegate and the methodgroup are compatible with the[EventHandler]
declaration and hence with the hardcoded event args type logic on both JS and .NET sides. However, custom rendertreebuilder code can skip this and use other delegate types, for example wiring up anonclick
event to anAction<EventArgs>
(I think). If the new logic is stricter about how the types must match, we might break the ability to declare polymorphic event handlers that downcast the incoming eventargs at runtime - a very obscure but possibly valid use case. I suspect we will end up breaking that use case unless we just hard-code rules about built-in event names.Examples
Basic custom event type
... with:
... with some JS-side config (these APIs are just sketches - real design to follow):
Of course, there also has to be something that actually triggers a DOM event called
onmycustomevent
on an element to make this really work end-to-end. Presumably people only do all this if they have some reason to be triggering custom DOM events on elements, for example they are integrating with custom elements, or are adding support for a new built-in browser event.Supplying extra data with a standard event
Note that
onpaste
is a standard event, whereasonpastewithdata
would be a specialised flavour of it:... with:
... with:
Detailed design
There are two goals, in this order of importance:
This ordering is because [2] can be regarded as a special case of [1]. As long as you can define
onmycustompaste
and have it occur when a "paste" happens, then you don't need to change the behavior of the defaultonpaste
.Sure it would be nice to do [2] as well because that's a more obvious match for what people think they need to do, but I'm going to argue we should keep it out of scope for now. Reasons later.
Defining entirely new event types
The Razor compiler already lets you define a custom event type, as follows:
During compilation it uses the existence of these attributes to know what intellisense to show if you put
@onmycustomevent
on any HTML element, and what code to generate (e.g., adding an attribute with valueEventCallback.Factory.Create<MyEventArgs>(yourCode)
).However, custom arguments don't work end-to-end because currently, there are two places where event args type information is hardcoded:
EventForDotNet.ts
, hardcoding the mapping from "DOM event object" (e.g., a JSClipboardEvent
instance) to an "event type" string (e.g.,clipboard
) and the shape of JSON data we want to send to .NET for this type of eventWebEventData.cs
, hardcoding the mapping that "event type" string (e.g.,clipboard
) to the .NET args type (e.g.,ClipboardEventArgs
), into which we deserialize the JSON dataSupporting custom event args means eliminating both sets of hardcoded rules, or at least letting the set be supplemented with custom rules.
Eliminating the hard-coded config on the JS side
I propose defining a new API like this:
Of course, by default we'll have registrations for the same set of standard web events already supported.
Why the custom logic is needed
I don't think there's any way to avoid the need for configuring this. We can't just serialize the entire JS
Event
object by default, as it's not serializable (references DOM elements, etc.). Likewise, developers have to make value judgements (e.g., for a clipboard event, are we going to serialize the pasted data no matter how large, or just give it an ID and send that to .NET?).Rules
Blazor.events.register
from JS code any time, e.g., when the application is first starting or when certain lazily-loaded component assembly initializes itself.onclick
) will throw. Likewise, they can't be unregistered. This is because non-uniqueness just won't compose across independent component libraries.Custom variants of standard events
We'll also support a further overload of
Blazor.events.register
that takes a separateDOMEventName
parameter:This treats the event name as
oncustompaste
as far as all interactions with .NET are concerned, but when registering the actual event listener with browser DOM APIs, uses the event nameonpaste
.The
DOMEventName
parameter values do not have to be unique.Question: Should we encourage some naming convention when customizing built-in events? For example, you could name your event
onpaste.special
instead ofonspecialpaste
- the Razor compiler is already fine with that. I suspect developers will feel better about it, and it composes nicely with directives like@onpaste.special:preventDefault
.Should we even bake that pattern in? That is, instead of having a separate overload of
Blazor.events.register
, we just impose the rule that if your event name contains a.
, then we only take the first segment to be the DOM event name, whereas the whole string is treated as the event name from the .NET perspective? It's pretty nice and simple.Eliminating the hard-coded config on the .NET side
For the .NET-side logic in
WebEventData.cs
, I propose more directly eliminating the notion of predefined event types, without needing any extra config. I believe the existing hardcoded set was done only because it was simple, not because it was necessary.We don't actually need JavaScript to tell .NET the event type, because .NET already has that information. It can look at the attached event handler delegate's type, and simply look at its parameter list via reflection.
Is it bad to use reflection for this? I don't think so:
Back-compat and polymorphic event handlers
Consider the following scenario:
Here, a single method accepts
EventArgs
and uses run-time type checks to behave differently for different events. You might be concerned that using the declared parameter list to choose how to deserialize the incoming JSON event data would break this, because we'd supply anEventArgs
only, and not either of the two subclasses.However because of how delegates work, this is actually not broken by the change I'm proposing. The
@onclick
attribute will compile as something that usesEventCallback.Factory.Create<MouseEventArgs>(HandleEvent)
, and hence produces a delegate instance of typeSystem.Action<MouseEventArgs>
. Likewise for@onfocus
but withFocusEventArgs
. So we actually deserialize the incoming JSON event data as the correct type based on the delegate (we don't actually use the parameter list of the method pointed to by the delegate).But there is a more obscure case where it would be broken. Consider this:
There's now only a single delegate instance, and it really does declare
EventArgs
as the parameter type, so that's all we'll now supply. That's technically a breaking change since before, the hardcoded logic would have suppliedMouseEventArgs
/FocusEventArgs
. The same issue happens if you're writingRenderTreeBuilder
logic manually and create event handlers from plain delegates instead ofEventCallback<T>
.This is an obscure scenario, but I suspect we still don't want a breaking change here. It's easy to fix though: for the built-in event names, we continue to have built-in hardcoded logic. We only use the delegate's declared parameter type for unrecognized event types.
Supplying custom data for built-in event types
I've already described above how developers could customize built-in event types by using alternate custom names for them (e.g.,
onspecialpaste
oronpaste.special
).Do we want to go further and provide a method for customizing built-in events without having to use custom unique names for this?
If we did this, it would have to compose well across independent 3rd-party libraries. It would have to be valid for two different libraries to customize (say)
onpaste
without interfering with each other. Somehow, we'd need to know which event handler corresponds to which library's customizations.My best idea for achieving this would be changing
Blazor.events.register
to be in terms of the args type name, instead of the event name. For example:Then, if you have this:
... the fact that you're using
MyPasteEventArgs
on the handler would trigger the corresponding JS-side code. This would compose beautifully, since each library would add additional ways to handle each event it cares about.However, one problem with this is getting the JS-side code to know which event args type corresponds to each event handler. The render tree frames don't have space for more per-event reference-type data. We could do it in a complicated way:
It's a bit annoying to have two different strategies to get the info, but we could do it.
It also has the drawback of disclosing a .NET type name to JS, which we try to avoid for Blazor Server. We could work around this by letting people optionally put some unique
[EventArgsTypeName("MyUniqueString")]
on the corresponding eventargs type. But this is getting hard to understand.Besides the above, there's one remaining killer problem. None of this solves the compile-time problem of teaching the Razor compiler how to know which
TEventArgs
to use in the generated code for@onclick=...
, if we're saying it's not uniquely determined by the event name. We'd need some entirely new kind of type inference that somehow allows the developer to use any one of the types registered via[EventHandler]
for that event name, possibly by codegenning overloads of some method that returns an EventCallback based on the supplied delegate. But this is a whole new level of complexity, and what would it mean for lambdas anyway? They'd immediately become ambiguous.Overall, while I think this would be a good feature, it could be really expensive to implement, so my proposal is we don't do it initially. We definitely have to support the "entirely custom event names" case, so let's just do that initially and see if people are happy with that. In the future, if people demand the "tweak the behavior of built-in event names" feature we could still add it separately (e.g., via some alternative
Blazor.events.registerEventArgsType
).Appendix: why .NET can't determine the event args type by using the
TValue
that was originally used withEventCallback.Factory.Create<TValue>(delegate)
First, that
TValue
information isn't stored on theRenderTreeFrame
instances in most cases. To reduce allocations we don't store the originalEventCallback<T>
structs (we'd have to box them). Preserving this data would mean either an extra allocation for every event handler on every render (not acceptable), or expanding theRenderTreeFrame
struct (bad as it would increase memory use for every bit of Blazor rendering everywhere, whether or not you use this feature, also diminishing cache locality, and would not be binary-compatible).Worse still, the
TValue
doesn't necessarily exist. It's only a detail of the Razor compiler. If you call theAddAttribute
method manually, you can supply an arbitrary delegate instance, not just anEventCallback
.Appendix: Alternative considered
The part of the above design I'm most uncomfortable with is relying on uniqueness of the event name, and having the JS-side API in terms of that event name. This may force people to have odd event names, and might lead to unnatural code if, in the future, we create even more extensibility in this area.
If we thought that defining and consuming custom events was a very mainstream part of the programming model, we might consider a more advanced alternative like the following:
[EventHandler]
attributes as a way of defining custom events, and replace them with extension methods. That is, to define an event, you create an extension method on some standard type likeM.A.C.EventHandlers
. For example,public static void OnClick(this M.A.C.EventHandlers eventHandlers, MyCustomEventArgs args)
.@using
.Blazor.registerEventArgsProvider('Fully.Qualified.MyCustomEventArgs', jsEvent => { /* return something JSON-serializable*/ });
.Pros of this approach:
@onclick
_Imports.razor
, or on a per-file basis)Cons of this approach:
@onclick
might be fine by default, but later adding@using SomeThirdPartyNamespace
might cause that existing code to break because the compiler now sees it as ambiguous.EventArgs
, we can't know which subclass it might cast to internally.Overall I do think this alternative model is more powerful and ultimately better, but since it's not a mainstream part of the programming model, I don't think the cost to implement and deal with leaving behind a redundant older extensibility model is justified.
The text was updated successfully, but these errors were encountered: