How ToT2 Dark_1200x303

It's straightforward to wire up event handlers to HTML events like onClick in your Blazor components, but how can you pass additional information and different types of arguments?

Let’s say you want to perform an action when a button is clicked in Blazor.

This is straightforward to do using Blazor’s event handling syntax.

You can wire up a method to an HTML element event and Blazor will invoke that method as an event handler for said event.

@page "/sayHello"

<button @onclick="GreetMe">Click me!</button>

@Message

@code {  
    string Message { get; set; }

    void GreetMe()
    {
        Message = "Hello";
    }    
}

Blazor will invoke GreetMe when the button is clicked, and the user will see a friendly (if somewhat unimaginative) greeting.

Your onclick event handler can optionally accept a MouseEventArgs parameter which Blazor will automatically include when it invokes the handler.

@code {  
    string Message { get; set; }

    void GreetMe(MouseEventArgs args)
    {
        if (args.AltKey)
            Message = "Greetings";
        else
            Message = "Hello";
    }    
}

Now we can easily access extra details about the button click, for example whether the user held down the ALT key when they clicked, in this case displaying a different greeting if so.

Different events will include different types of event args—check out official docs for a complete list.

Passing Additional Data to your Event Handlers

So far so good, but what if you need to pass additional data to your event handlers?

For example, you might need to loop through a collection and render buttons for each item.

@page "/todoList"
@using System.Diagnostics

@foreach (var todo in Todos)
{
    <p>@todo.Text</p>
    <!-- delete button goes here -->
}

@code {
    List<Todo> Todos { get; set; } = new List<Todo>
    {
        new Todo {Id = 1, Text = "Do this"},
        new Todo {Id = 2, Text = "And this"}
    };

    void Delete(Todo todo)
    {
        Debug.WriteLine($"Deleting {todo.Id}");
    }

    private class Todo
    {
        public int Id { get; set; }
        public string Text { get; set; }
    }
}

This example iterates over a list of Todos and shows text for each one.

But what if we want to delete a Todo? We need a way to indicate which Todo should be removed when we click a button for that specific Todo.

The standard way of calling an EventHandler won’t work because we can’t pass additional arguments.

<button @onclick="Delete">X</button>

Delete would have no idea which Todo to delete!

The answer is to use a lambda which will then delegate to our Event Handler.

@foreach (var todo in Todos)
{
    <p>@todo.Text</p>
    <button @onclick="() => Delete(todo)">X</button>
}

We’ve told Blazor to invoke an anonymous expression (represented here using the lambda syntax) which in turn calls Delete, passing the current Todo instance along for the ride.

Our Delete method will receive the relevant instance of Todo and can take whatever action it deems necessary.

void Delete(Todo todo)
{
    Debug.WriteLine($"Deleting {todo.Id}");
    // hit the database and delete the todo
}

What if You Need the Original EventArgs as well?

What if you need both the original event args (such as MouseEventArgs) and your own arguments (such as which Todo to delete).

For this, you can reference the original event args in your lambda and forward them along.

<button @onclick="args => Delete(todo, args)">X</button>

This effectively forwards the arguments from the original event to your event handler, where you can do with them as you please.

void Delete(Todo todo, MouseEventArgs args)
{
    Debug.WriteLine($"Alt Key pressed: {args.AltKey} whilst deleting {todo.Id}");
}

Gotcha—Loops and Lambdas

Using lambdas inside forEach loops in your Blazor components will generally work as you’d expect, but watch out if you decide to use a regular for loop with a loop variable instead.

@page "/loops"
@using System.Diagnostics

@for (int i = 0; i < 5; i++)
{
    <button @onclick="()=>Log(i)">@i</button>
}

@code {
    void Log(int i)
    {
        Debug.WriteLine($"Logging: {i}");
    }
}

In this example we’re simply looping from 0 to 5 and rendering a button each time.

When you click the button you’ll see the value of the loop variable i logged out in the debug console (in your IDE when you debug your app).

Now, pause for a moment and consider what you’d expect to see when you run this code and click any of the buttons…

You’d be forgiven for expecting to see different numbers when you click different buttons.

But in reality you’ll see…

Console output showing results of using loop variable directly

…a whole load of 5s!

Whichever button you click, the debug output tells you i is always 5.

Strange huh?!

This isn’t a Blazor thing, but actually the way anonymous expressions and loops work in C#.

If you just want to solve this and move on with your day, capturing i in a local variable inside the loop will do the trick.

@page "/loops"
@using System.Diagnostics

@for (int i = 0; i < 5; i++)
{
    var j = i;
    <button @onclick="()=>Log(j)">@i</button>
}

@code {
    void Log(int i)
    {
        Debug.WriteLine($"Logging: {i}");
    }
}

Here we capture the value of i in j, then use j in our lambda expression.

To understand why this happens requires a detour into the secret world of your C# compiler!

Understanding C# Loops and Lambdas

Here’s a pseudo-code representation of the compiled C# code that will be executed for our Blazor component. Typically we don’t need to pay much attention to this, but in this case it’s useful to see what’s actually going on “under the hood”.

[Route("/loops")]
public class Loops : ComponentBase
{
    void BuildRenderTree(RenderTreeBuilder _builder)
    {
        Loops.DisplayClass displayClass = new Loops.DisplayClass();
        displayClass.this = this;
        for (displayClass.i = 0; displayClass.i < 5; displayClass.i++)
        {
            _builder.openElement('button');
            _builder.AddAttribute<MouseEventArgs>
                ("onclick", new Action(displayClass.handle));
            _builder.closeElement('button');
        }
    }

    private void Log(int i)
    {
        Debug.WriteLine(string.Format("Logging: {0}", (object) i));
    }

    [CompilerGenerated]
    private sealed class DisplayClass
    {
        public int i;
        public Loops this;

        internal void handle()
        {
            this.Log(i);
        }
    }
}

I’ve removed some detail and simplified the actual code to keep this readable, but the gist is as follows.

The compiler generates a BuildRenderTree method for your component.

When this component is rendered, this method creates a single instance of something called DisplayClass.

Note how BuildRenderTree only creates one instance of this class (outside the loop), then references this single instance inside the loop.

Every time it goes round the loop, it will add a button to the RenderTreeBuilder and add an onclick attribute for this button pointing to an Action.

Crucially, each action points to the same single instance of DisplayClass.

DisplayClass.i is then incremented and we go around the loop again.

By the time we’ve been around the loop 5 times, the final number stored in i in the display class will be 5.

Now, when you run the app and click any of the buttons, all the actions are pointing to the single instance of DisplayClass containing the value i (5 in this case).

So it’s not surprising you see a lot of 5s for every button click!

The trick to overcome this is to capture the value of i into a local variable inside the for loop.

@page "/loops"
@using System.Diagnostics

@for (int i = 0; i < 5; i++)
{
    var j = i;
    <button @onclick="()=>Log(j)">@i</button>
}

In the resulting compiled code, a new instance of DisplayClass will now be created every time round the loop.

void BuildRenderTree(RenderTreeBuilder _builder)
{
    for (int index = 0; index < 5; ++index)
    {
		Loops.DisplayClass displayClass = new Loops.DisplayClass();
    	displayClass.this = this;
        displayClass.j = index;
        _builder.openElement('button');
        _builder.AddAttribute<MouseEventArgs>
            ("onclick", new Action(displayClass.handle));
        _builder.closeElement('button');
    }
}

[CompilerGenerated]
private sealed class DisplayClass
{
    public int j;
    public Loops this;

    internal void handle()
    {
        this.Log(j);
    }
}

This avoids the shared i value problem, and clicking the buttons works as you would expect.

In Summary

Blazor enables you to handle HTML events such as onClick events via event handlers.

You can use lambdas to capture values (for example in a loop) and pass them on to your event handlers.

Different events pass different types of event args so check out the official docs for details.

Watch out for those pesky for loops; if you use the loop variable without capturing it to a local variable inside the loop you’ll fall foul of the C# compiler and see behavior you probably don’t want or expect!


Jon Hilton
About the Author

Jon Hilton

Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.

Related Posts

Comments

Comments are disabled in preview mode.