Figuring out how technologies are maid under the hood is one of the things I love to do. Ever since I started using Blazor and going in-depth into its features, I can’t stop wondering how things are made internally.
This post is the start of a series about, you guess it, Blazor Internals. I’ll be talking about things that I find interesting in Blazor and how they work under the hood by reading the source code.
I’d also like to point to the amazing blog of Safia Abdallah, an engineer at Microsoft that works on Blazor. She has some great posts about Blazor’s internals too.
What is an EventCallback
EventCallback (and its generic version
EventCallback<T>) are structs that are declared in a component as parameters (using
[Parameter]). Consumers of that component can “subscribe” to those parameters so that they are “notified” whenever they are triggered. You can think of them as a small / straightforward version of C# events.
Let’s take as an example a
As you can see, we declare a
OnClick parameter of type
EventCallback. We want the users of this component to be able to subscribe to the button’s click event. Whenever the button is clicked, we call
OnClick.InvokeAsync() to notify the subscriber (if there is one).
We can use it this way:
What’s interesting about it
Unlike regular C# events,
EventCallback can’t be null (since it’s a
struct) and can only have a single subscriber. This works well for Blazor since you almost always only need one subscriber (which is usually the parent).
If you paid attention to the examples above (our
Button component and its user), you will find that I declared the first handler as an
async Task and the second one as
void. This is what’s great about
EventCallback, your handlers can either be asynchronous (
Task) or not (
How does it work
For example, when you subscribe to the
@onclick event, which is an
EventCallback<MouseEventArgs>, in your component (
@onclick="SomeMethod"), it will get compiled to something like this:
The same goes for the non generic version of
EventCallback, everything is created using
EventCallback.Factory.Create, which handles many types of methods:
This is how event handlers can be asynchronous or not.
EventCallbackFactory contains all these overloads for the different use cases.
The next logical question is: How does
EventCallback store the delegates when their signature isn’t the same? Easy:
All delegates (
Func, your own) are
MulticastDelegates, which are
Delegates. This makes it easy to handle multiple types of methods (different signatures). See MulticastDelegate Class for more information about it.
Here’s an example to better understand how it works:
As demonstrated, you can cast all delegates to
MulticastDelegate and back to their origin type.
Whenever Blazor wants to trigger the event callback, it calls
EventCallback.InvokeAsync. This calls
Receiver.InvokeAsync with a new
EventCallbackWorkItem and an argument (in case it’s the generic version). See the source code.
EventCallbackWorkItem is the star struct here:
As you can see,
EventCallbackWorkItem executes the
MulticastDelegate by first trying to cast it to known delegates (
Func). This way the performance is the same as calling the methods directly. The fallback is to call
DynamicInvoke, which I personally don’t know when it can happen, but if it does, it’s a big performance hit.
Here’s how the receiver handles the event:
After the event is triggered,
StateHasChanged is called. If the event handler returned an incomplete
StateHasChanged is called again after the task is finished.
Some Blazor developers, including me, don’t like this automatic SHC call and prefer to have an option to disable it. I believe the Blazor team is working on it.
At first, you might think that some functionalities are so hard to implement and scary to understand, until you dive deep into them and it turns out to be a fun journey.
Event callbacks are a great feature that is used everywhere, it’s only fair we understand how it works under the hood.
I hope you enjoyed this post, stay tuned for the next ones!