Closed SteveSandersonMS closed 2 years ago
Thanks for putting this together, @SteveSandersonMS. I was a bit hesitant at first regarding the auto-re-rendering after each handler completes, but after some more thoughts I believe what you have is great.
Given <input @bind=A @onchange=B>, the existing compiler emits diagnostic RZ10008 and fails compilation. We will remove this diagnostic completely.
Presumably this needs to be behind a LangVersion=7.0
switch since it requires runtime changes?
MulticastEventCallback
makes sense. The slight iffy part is that EventCallback wraps MulticastDelegate
which kinda has the semantics we want (if you squint at it).
Overall I like this.
Presumably this needs to be behind a LangVersion=7.0 switch since it requires runtime changes?
You're probably right!
The slight iffy part is that EventCallback wraps MulticastDelegate which kinda has the semantics we want (if you squint at it).
Yeah, I did for a while try to work out a way of just using MulticastDelegate
to model this, but it doesn't really work because it would be a breaking change for anyone who's already doing interesting things with MulticastDelegate
. They already have the behavior that we treat it as returning a single Task
, but the new version has to receive a set of Task
instances so we can re-render after each handler.
However today I've been reflecting on this further and have started to think this whole "multiple event handlers" concept is solving the wrong problem. I'm now investigating if we can do something very different (and more specific to @bind
) which would solve a broader range of problems.
@pranavkm I've written up a very different solution strategy at https://github.com/dotnet/aspnetcore/issues/39837. If we go for that, it would make this "multiple event handlers" concept entirely unnecessary.
Closing in favour of https://github.com/dotnet/aspnetcore/issues/39837
Summary
Blazor should allow HTML elements and components to accept multiple event handlers for a single event name.
Motivation and goals
This was requested at https://github.com/dotnet/aspnetcore/issues/14365 and is well upvoted. It deals with the following pain points:
@bind
and@onchange
(or other bind-supported events) at the same time, letting developers perform additional actions before or after bindingaddEventListener
.Really, the case with
@bind
is the important one, as it's fairly common and existing workarounds (such as custom property setters) are very messy and don't even work if you're trying to perform an async action.In scope
@attributes
overrides or is overridden by another event handler.ComponentBase
components should re-render after each async handler's returnedTask
, not waiting for them all to complete. However, there should not be a synchronous re-render after invoking each handler, as it sufficies to do a single synchronous render after they have all started.<button @onclick=A @onclick=B>
, the existing tooling shows a squiggly-line warning for the second (the code compiles, but produces invalid logic that throws at runtime). Tooling needs to stop showing a warning.<input @bind=A @onchange=B>
, the existing compiler emits diagnosticRZ10008
and fails compilation. We will remove this diagnostic completely.@onchange
handlers can go on a single element.Out of scope
AddAttribute
twice with the same event handler name. The existing behavior is to only run the second handler. This is arbitrary and unsupported, so it's OK for the behavior here to change.Components receiving multiple values for arbitrary parameters. So, when used on components (not elements) this only affects the
CaptureUnmatchedValues
behavior. Examples:<MyComponent @onclick=A @onclick=B />
<MyInput @bind-Value=A @onchange=B>
<MyInput @bind-Value=A OnChange=B>
<MyComponent OnClick=A OnClick=B>
.This is because, for arbitrary parameters like
OnClick
, there's no built-in concept that they represent events. There's no strong use case forOnClick=A OnClick=B
, as the person doing this can always just make a single method that callsA
andB
.Razor syntax for customizing whether
@attributes
overrides or appends to the set of handlers for a given event name.<button @onclick=A @attributes=B>
. IfB
contains anonclick
, this should only call that one, and notA
. This is needed for back-compat.<button @attributes=A @onclick=B>
. This should only callB
, regardless of what's inA
. This is needed for back-compat.Developers who want arbitrary rules for appending or replacing event handlers should be able to do so by cracking open the
CaptureUnmatchedValues
dictionary by hand, but we won't extend the build-in@attributes
syntax for this.Risks / unknowns
<input @onchange="MyAsyncLogic" @bind="SomeValue">
, developers might think that - becauseMyAsyncLogic
comes first - we'd wait arbitrarily long for its async task to complete before updatingSomeValue
. This is not the case. Conceptually, multiple event handlers will run in parallel, even though you can control the order in which they start. This is because:@bind
if there were asynchronous delays before it. Blazor Server is extremely careful about this to ensure that keystrokes are never lost and the UI doesn't revert to a prior state when performing unrelated re-renders.@onclick=A @onclick=B
that you should also be able to do@ref=A @ref=B
or@key=A @key=B
orSomeComponentParam=A SomeComponentParam=B
. None of those other cases are allowed, mostly because they don't have clear meanings and nobody wants them.Examples
The most classic use case is to perform some action before or after binding.
Without this feature, you have to do some really nasty stuff to achieve the above, e.g., binding to a property with a custom setter and then discard the
Task
.A more complex variant of the above is with component binding:
Note that this doesn't solve the problem that we're discarding the
Task
returned byValueChanged.InvokeAsync
, but it does mean thatBeforeValueChanged
andAfterValueChanged
can have correct async behaviors and not discard their tasks.Detailed design [OPTIONAL - only read if you don't have questions/feedback about the above]
I think we already have consensus that the feature is desirable, so I'm going to sketch some design thoughts. These could change if we end up changing any of the scope and goals.
Representing multiple event handlers
When we have
<button @onclick=A @onclick=B>
, how is the set of event handlers stored internally? This is a key question that underpins most of the possible implementation choices. I can think of three main choices:A. Multiple values with the same name. For example, there would be two
RenderTreeFrame
entries both with theAttributeName
valueonclick
.B. Multiple values with different names. For example, there would be two
RenderTreeFrame
entries, and the compiler would produce mangled names likeonclick
andonclick.1
.C. Single value encapsulating all handlers. For example, there would continue to be a single
RenderTreeFrame
entry, and its value would represent all the handlers.Con: how does it represent multiple handlers?
This leads to sub-cases:
A
thenB
, and uses that as the delegate.Task
, how can we re-render after each inner handler? How do we make it preserve ErrorBoundary semantics if the handlers have different targets?Task
from each.However, the fact that we also want to support this on component parameters (not just HTML elements) with
CaptureUnmatchedValues
imposes further restrictions that eliminates some options. Consider<MyComponent @onclick=A @onclick=B>
. The component is going to receive anIDictionary<string, object>
representing the attributes. So:Between C1 and C2, it's fairly clear that C2 is more realistic. So let's imagine that one in more detail.
Introducing MulticastEventCallback
Invent a new kind of event callback that represents an ordered set of mutually-compatible event callbacks.
Sidenote on naming
We could call it:
EventCallbackGroup
EventCallbackCollection
MulticastEventCallback
They need to be mutually compatible in the sense that there has to be a single most-specific eventargs type, since the client is only calling the event once and passing a single eventarg. So, the set can include
EventCallback
,EventCallback<ChangeEventArgs>
andEventCallback<ChangeEventArgsSubclass>
(and the client would be asked to sendChangeEventArgsSubclass
). But it can't includeEventCallback<ChangeEventArgs>
andEventCallback<MouseEventArgs>
, as there's no most-specific type.So it's not an arbitrary set, which leads me to prefer the name
MulticastEventCallback
over the group/collection names. It hints at the "single input, multiple output" nature of this concept.Generated code
Given
<button @onclick=A @onclick=B>
, we'd emit:... where we also define:
Since the generated code still uses existing
EventCallback.Factory.Create<T>
to produce theEventCallback
values, all the existing logic around overload selection and generic type inference will continue to work.Another benefit from
MulticastEventCallback
being public API is that component authors who have complex custom requirements can build arbitrary logic around it. For example, they can have arbitrary rules forCaptureUnmatchedValues
appending, prepending, or overwriting other handlers if they look inside the supplied dictionary and construct their ownMulticastEventCallback
value to use as an@onevent=...
value.Notes for me:
MulticastEventCallback
-typed attribute values.Dictionary<ulong, EventCallback> _eventHandlers
will have to change toDictionary<ulong, MulticastEventCallback>
. This should not cause any extra allocations for existing single-handler cases.