Code Focused

Blazor: Working with Events

In this article I describe how to work with events in your Blazor applications. I cover two types of events, DOM events and custom or user-defined events. DOM events are things such as onclick or onchange and are triggered by a user interaction of some kind. User-defined events are defined by the developer based on the needs of the app.

DOM Events
Way back in the Blazor 0.1 days events were extremely limited; we had to make do with only three: onclick, onchange and onkeypress. The event data available was also rather basic -- in fact I'm not even sure if we could access any event-specific information. But with the release of Blazor 0.2 things changed dramatically.

In Blazor 0.2 the way events were handled was given an overhaul. Now any event was available to developers and there was even specific event data available (depending on the event). Since then, more event-specific data has been added, and it's a pretty good experience working with events in Blazor now.

In the current version of Blazor developers can access any event by using the on<event> attribute with an HTML element. The attribute's value is treated as an event handler. In the following example the LogClick() method is called every time the button is pressed:

<button onclick=@LogClick>Press Me</button>

@functions {
  private void LogClick()
  {
    Console.WriteLine("Button Clicked!");
  }
}

It is also possible to use Lambda expressions, so the example above could be written as follows:

<button onclick="@(() => Console.WriteLine("Button Clicked!");)">
  Press Me
</button>

Event Data
While it is great to have access to all these events, some are not much use without some data to go with them. For example, we may want something specific to happen when a user presses the "r" key in an input. So we go ahead and wire up the event as below:

<input type="text" onkeypress=@KeyWasPressed />

@functions {
  private void KeyWasPressed()
  {
    Console.WriteLine("r was pressed");
  }
}

But there's a problem. The event is going to fire every time any key is pressed. We only wanted to print the message when the "r" key was pressed. This is where we need some data on the event.

Blazor provides a set of event argument types that map to events. Below is a list of event types and the event argument type they map to. If you want to see which specific event argument type each event has you can view them here.

  • Focus Events - UIFocusEventArgs
  • Mouse Events - UIMouseEventArgs
  • Drag Events - UIDragEventArgs
  • Keyboard Events - UIKeyboardEventArgs
  • Input Events - UIChangeEventArgs/UIEventArgs
  • Clipboard Events - UIClipboardEventArgs/UIEventArgs
  • Touch Events - UITouchEventArgs
  • Pointer Events - UIPointerEventArgs
  • Media Events - UIEventArgs
  • Progress Events - UIProgressEventArgs

To solve our problem, we need to make use of the UIKeyboardEventArgs. To gain access to this event data, we need to make a small change to the code:

<input type="text" onkeypress="@(e => KeyWasPressed(e))" />

@functions {
  private void KeyWasPressed(UIKeyboardEventArgs args)
  {
    if (args.Key == "r")
    {
      Console.WriteLine("R was pressed");
    }
  }

By changing to a Lambda expression we can now pass the event data to the event handler. With this data we are able to access the Key property, which contains a string of the key that was pressed to fire the event. A simple if statement is all that is needed and we are now only getting messages in the console when the "r" key is pressed.

User-Defined Events
Now that we've covered DOM events, let's talk about defining our own events. Like many things in software development, the need for defining your own events will very much depend on your situation.

As an example I'm going to use a state store, a pattern becoming common in modern single-page applications (SPA). For those who are not familiar, the idea is to have a single place that stores and performs operations on the state of your application.

Let's take an expense tracking app that records money spent and gives a running total. We can define a simple state store like this:

public class AppState
{
  private readonly List<Expense> _expenses = new List<Expense>();
  public IReadOnlyList<Expense> Expenses => _expenses;

  public event Action OnExpenseAdded;

  public void AddExpense(Expense expense)
  {
    _expenses.Add(expense);
    StateChanged();
  }

  private void StateChanged() => OnExpenseAdded?.Invoke();
}

In the code above, the key part for us is the custom OnExpenseAdded event. Whenever a new expense is added to the collection, this event is going to be invoked. Now let's build a couple of components that can use the store.

The first is just a simple component that records the expense data and then adds that to the collection on the state store:

@inject appState

<h2>Add Expense</h2>
<div class="row">
  <div class="col-md-4">
    <input class="form-control" bind="@expense.Description" placeholder="Enter Description..." />
  </div>
  <div class="col-md-2">
    <input class="form-control" bind="@expense.Amount" placeholder="Enter Amount..." />
  </div>
  <div class="col-md-6">
    <button onclick="@AddExpense" class="btn btn-primary">
      Add
    </button>
  </div>
</div>

@functions {
  private Expense expense = new Expense();

  private async Task AddExpense()
  {
    await appState.AddExpense(expense);
    expense = new Expense();
  }
}

With that in place, all that we need is a component to display the total of our expenses:

@inject appState

<h2>Current Total: £@totalExpenses</h2>

@functions {
  private decimal totalExpenses => appState.Expenses.Sum(x => x.Amount);

  protected override void OnInit()
  {
    appState.OnExpenseAdded += StateHasChanged;
  }
}

As you can see, I've registered the components StateHasChanged method to our custom event in the state store. Now whenever an expense is added, this component will update itself and show the new total for the expenses.

Before I sign off I just wanted to point out some great news for Blazor fans. At .NET Conf it was officially announced that Blazor's server-side model will be shipped as part of ASP.NET in .NET Core 3! [see article here] I do want to point out though that the team is still committed to delivering the client-side model. However, it will be staying in an experimental state for now while work on running .NET on WebAssembly continues.

As a reminder, I explained how to consume Web APIs in Blazor in last month's article.

About the Author

Chris Sainty is a lead software engineer at Flagship Group, a housing association based in Norwich, Norfolk (UK). He leads a team in developing in-house repairs management software using a wide range of technologies. Chris is passionate about Web technologies and ASP.NET Core in particular. He loves sharing knowledge and writes regular posts on his blog at https://codedaze.io.

comments powered by Disqus

Featured

Subscribe on YouTube