question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Design proposal: Multiple same-named event handlers

See original GitHub issue

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:

  • Being able to use @bind and @onchange (or other bind-supported events) at the same time, letting developers perform additional actions before or after binding
  • More generally reaching parity with HTML elements and addEventListener.

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

  • Back-compat in all cases with Razor syntax. All existing valid RZC-emitted code should not be able to observe any change of behavior. This includes when @attributes overrides or is overridden by another event handler.
  • Developer controls the order of handler execution. We’ll use the left-to-right source code order. The diffing algorithm mustn’t lose this order information.
  • Each handler should behave independently of others. For example:
    • ComponentBase components should re-render after each async handler’s returned Task, 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.
    • Each handler may have a different target, and hence any subset of those targets may transition their enclosing ErrorBoundary into an error state independently of the others, and that doesn’t stop subsequent handlers from running.
  • Compiler/tooling error updates
    • Given <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.
    • Given <input @bind=A @onchange=B>, the existing compiler emits diagnostic RZ10008 and fails compilation. We will remove this diagnostic completely.
  • Having as many handlers as you want. Even though there are very few use cases, it will be easiest to understand if there’s no upper bound on how many @onchange handlers can go on a single element.
  • Pay only if you use it. There should be no perf degradation (e.g., extra allocations, or any new multi-step process during diffing) for the single-handler case. However it’s OK for this new functionality to incur some modest extra cost since it will be rarely used.

Out of scope

  • Back-compat in edge cases with manual RenderTreeBuilder logic.

    • Example: currently, you can call 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.
    • As it happens, my proposed design would retain back-compat even here, but that’s just lucky and not a goal
  • Components receiving multiple values for arbitrary parameters. So, when used on components (not elements) this only affects the CaptureUnmatchedValues behavior. Examples:

    • Will work: <MyComponent @onclick=A @onclick=B />
    • Will work: <MyInput @bind-Value=A @onchange=B>
    • Will work: <MyInput @bind-Value=A OnChange=B>
    • Won’t work: <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 for OnClick=A OnClick=B, as the person doing this can always just make a single method that calls A and B.

  • Razor syntax for customizing whether @attributes overrides or appends to the set of handlers for a given event name.

    • Example: <button @onclick=A @attributes=B>. If B contains an onclick, this should only call that one, and not A. This is needed for back-compat.
    • Example: <button @attributes=A @onclick=B>. This should only call B, regardless of what’s in A. 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

  1. Given <input @onchange="MyAsyncLogic" @bind="SomeValue">, developers might think that - because MyAsyncLogic comes first - we’d wait arbitrarily long for its async task to complete before updating SomeValue. 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:
    • It’s necessary to satisfy the principle of “each event handler behaves independently”, which in turn is necessary to make it practical to reason about.
    • It would completely wreck the behavior of @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.
  2. It might complicate things in the future if we decide to allow JS-style event mutation, e.g., for a handler to cancel the event. We could probably at least do whatever JS does.
  3. Developers might think that, because you can do @onclick=A @onclick=B that you should also be able to do @ref=A @ref=B or @key=A @key=B or SomeComponentParam=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.

<input @bind="searchText" @onchange="PerformSearch" />

@code {
    string searchText;

    async Task PerformSearch()
    {
        // ... use the updated searchText value ...
    }
}

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:

<input @onchange="BeforeValueChanged" @bind="BoundValue" @onchange="AfterValueChanged" />

@code {
    string BoundValue
    {
        get => Value;
        set => ValueChanged.InvokeAsync(value);
    }

    [Parameter] public string Value { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }

    [Parameter] public EventCallback<ChangeEventArgs> BeforeValueChanged { get; set; }
    [Parameter] public EventCallback<ChangeEventArgs> AfterValueChanged { get; set; }
}

Note that this doesn’t solve the problem that we’re discarding the Task returned by ValueChanged.InvokeAsync, but it does mean that BeforeValueChanged and AfterValueChanged 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 the AttributeName value onclick.

  • Pro: feels like this is how you’d design it from scratch.
  • Con: every aspect of the system, such as diffing, event tracking, and event dispatch, all would have to change.

B. Multiple values with different names. For example, there would be two RenderTreeFrame entries, and the compiler would produce mangled names like onclick and onclick.1.

  • Pro: some existing diffing and event tracking/dispatch logic would just work because the names are distinct
  • Con: diffing’s attribute overriding logic would get very confused, and we might incur extra runtime costs to preserve ordering when doing the dictionary-based attribute diffs
  • Con: customer code that cares about attribute names would encounter new cases it perhaps doesn’t handle

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.

  • Pro: all existing diffing logic would just work
  • Pro: naturally retains existing overriding rules for back-compat
  • Con: how does it represent multiple handlers?

This leads to sub-cases:

  • C1. Razor compiler codegens a single method that calls A then B, and uses that as the delegate.
    • Pro: no new runtime logic needed - it’s all in the compiler
    • Con: extremely hard or impossible to generate code that works as desired. Since the generated delegate can only return a single Task, how can we re-render after each inner handler? How do we make it preserve ErrorBoundary semantics if the handlers have different targets?
  • C2. Define some new event callback type that represents a collection of event callbacks. The renderer can know about this, and call each of its inner handlers in order, receiving a separate Task from each.
    • Pro: relatively simple change to Razor compiler
    • Con: another concept to wield inside the renderer

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 an IDictionary<string, object> representing the attributes. So:

  • Option A doesn’t work, because the dictionary can’t have multiple entries with the same key
  • Option B is really bad, because the mangled names become visible to user code, which can get confused about them or take a dependency on the exact form of mangling
  • Option C is fine with this

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> and EventCallback<ChangeEventArgsSubclass> (and the client would be asked to send ChangeEventArgsSubclass). But it can’t include EventCallback<ChangeEventArgs> and EventCallback<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:

builder.OpenElement(0, "button");
builder.AddAttribute(1, "onclick", new MulticastEventCallback(
    EventCallback.Factory.Create<MouseEventArgs>(this, A).AsUntyped(),
    EventCallback.Factory.Create<MouseEventArgs>(this, B).AsUntyped()));
builder.CloseElement();

… where we also define:

// It's a struct because, even though it always gets boxed when stored on a RenderTreeFrame, it makes the storage inside Renderer's dictionary cheaper (or at least better memory locality).
public readonly struct MulticastEventCallback
{
    public MulticastEventCallback(params[] EventCallback callbacks) { ... }

    // Of course we could also special case overloads for one param, two params, etc., if it turns out to reduce the allocations on calling it. Internally we might have a fixed number of EventCallback fields along with an array field that's only used if there are too many.
}

Since the generated code still uses existing EventCallback.Factory.Create<T> to produce the EventCallback 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 for CaptureUnmatchedValues appending, prepending, or overwriting other handlers if they look inside the supplied dictionary and construct their own MulticastEventCallback value to use as an @onevent=... value.

Notes for me:

  • Then we need to update some aspects of event handler assignment and event dispatch logic to know about MulticastEventCallback-typed attribute values.
  • In fact, the renderer’s underlying storage of Dictionary<ulong, EventCallback> _eventHandlers will have to change to Dictionary<ulong, MulticastEventCallback>. This should not cause any extra allocations for existing single-handler cases.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:2
  • Comments:5 (5 by maintainers)

github_iconTop GitHub Comments

4reactions
SteveSandersonMScommented, Jan 28, 2022

@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.

0reactions
SteveSandersonMScommented, Jan 31, 2022
Read more comments on GitHub >

github_iconTop Results From Across the Web

Support multiple event handlers for the same event ...
Support multiple event handlers for the same event on the same element #14365 ... Design proposal: Multiple same-named event handlers #39815.
Read more >
Assign multiple event handlers to single event
I have another function newFun() to assign to the same event. What I want to happen is when the document.onmousemove event occurs both...
Read more >
Domain events: Design and implementation
There can be several handlers for the same domain event in the Application Layer, one handler can solve consistency between aggregates and ...
Read more >
Integration Services (SSIS) Event Handlers - SQL
When the Event Handlers tab is active, the Control Flow Items and Maintenance Plan Tasks nodes of the Toolbox in SSIS Designer contain...
Read more >
Event Handlers
1 Introduction Event handlers define microflows that handle certain events related to the entity. Depending on the chosen moment and type, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found