Executing async operations onchange in Blazor

Blazor's two-way databinding model is extremely powerful, but sometimes it can get in the way of what you want to do. For example, you might want to execute an asynchronous operation such as remote validation when a value changes in a textbox. Perhaps you want to check instantaneously that the value - a username maybe - doesn't already exist in a database before you accept a new registration. You cannot add an onchange event handler if you already have a value bound to the element, so what can you do? In this article, I look at solutions for both HTML elements and EditForm input validation components up to an including .NET 7, and a new feature released in .NET 7.0.1.

First, a refresher on the workings of databinding in Blazor.  Here, I'm referring to binding a value to a form control or a form input validation component. We use @bind or @bind-value to bind a data item to a standard HTML form control, or @bind-Value to achieve the same result with an input validation control (one that derives from InputBase)

<input class="form-control" @bind="username" />
<InputText class="form-control" @bind-Value="Account.Username" />

Behind the scenes, this establishes a two-way relationship between the data item and the control. Any change to the value of the control is propagated to the data item, and any change to the value of the data item is reflected in the value of the control:

2 way data binding in Blazor

When you use the @bind or @bind-Value directive, Blazor generates an onchange event handler behind the scenes which is used to update the value of the bound data item. This is great, but if you want to execute some code when the value changes, you cannot do so by adding your own onchange event handler to the control. If you try to add your own, you get a Razor compiler error:

RZ10008 The attribute 'onchange' is used two or more times for this element. Attributes must be unique (case-insensitive). The attribute 'onchange' is used by the '@bind' directive attribute.

If you want to execute a synchronous operation when the onchange event fires, you can bind a property and hook into its setter. In the following example, the value bound to the form control earlier is changed to lower case when the setter for username executes:

string _username;
string username 
{ 
    get => _username;
    set
    { 
        _username = value?.ToLower(); 
    }
}

But there is no support for asynchronous code in property setters, and you might, for example, want to make an asynchronous method call to an API when the value changes. One solution is to lose two way binding. Then you can add an onchange event handler to the control yourself, but you must now take responsibility for updating the value of the bound data item. Here's the code for the relevant control. Note that now I'm using one-way binding to the value attribute:

<input class="form-control" value="@username" @onchange="CheckUsername" />
<span class="d-block text-danger">@message</span>

When the onchange event fires, the async CheckUsername method is executed:

async Task CheckUsername(ChangeEventArgs e)
{
    message = string.Empty;
    if (e.Value is not null)
    {
        username = e.Value.ToString();
        var valid = await http.GetFromJsonAsync<bool>($"/checkusername?username={username}");
        if (!valid)
        {
            message = "You must choose another user name";
        }
    }
}

I access the ChangeEventArgs and obtain the value of the contol. Then I set username to whatever the form control value is, and call my API to check whether the provided value is unique.

EditForms

If you are working with input validation components within an Editform, the approach you take will be a little different. You could provide a function to the OnSubmit parameter and execute async code within that. However, that function will only execute when the form is submitted. In this article, we are looking at executing async code OnChange. Prior to .NET 7.0.1, there are two ways to execute async code when the value changes in a component that implements InputBase, such as the InputText component. The first is to provide a function to the ValueChanged parameter, and the second is to hook into the OnFieldChanged event of the EditContext.

ValueChanged

In this example, we make use of the ValueChanged parameter of a control that derives from InputBase (any of the validation input controls you use with an EditForm). This takes a callback in which we can query our API. Just as with the HTML element example, we cannot specify values for @bind-Value if we provide one for ValueChanged, so we lose two-way databinding. Consequently, we must also update the value that is bound to the control. And we need to update the validation status of the EditContext. So, rather than pass a model to the EditForm, we pass an explicit EditContext. We also instantiate a ValidationMessageStore so that we can access it:

Account Account = new();
EditContext editContext;
ValidationMessageStore modelState;
 
protected override void OnInitialized()
{
    editContext = new EditContext(Account);
    modelState = new ValidationMessageStore(editContext);
}

Here's the form. Note that we must also provide values to the Value and the ValidationExpression parameters on the InputText component. It is a verbose approach:

<EditForm EditContext="editContext">
    <label class="form-label">Enter a user name</label>
    <InputText class="form-control" 
        Value="@Account.Username" 
        ValueExpression="()=>Account.Username" 
        ValueChanged="(string s)=>CheckUsername(s)" />
    <ValidationMessage For="() => Account.Username" />
</EditForm>

Here's the CheckUserName callback:

async Task CheckUsername(string input)
{
    if (!string.IsNullOrWhiteSpace(input))
    {
        Account.Username = input;
        modelState.Clear(editContext.Field(nameof(Account.Username)));
        var valid = await http.GetFromJsonAsync<bool>($"/checkusername?username={input}");
        if (!valid)
        {
            modelState.Add(editContext.Field(nameof(Account.Username)), "You must choose another user name");
        }
        editContext.NotifyValidationStateChanged();
    }
}

If a value has been provided, we have to update the value bound to the component. Then we clear any existing validation entry relating to this field before executing the remote asynchronous call. Depending on the result, we add a new validation message for this field, and then let the EditContext know that something may have changed either way.

OnFieldChanged

We can also add an event handler to the OnFieldChanged event of the EditContext. This event fires whenever the value of any field in the EditForm is modified, so we must check to determine which field was changed before we execute our remote API call. We set up the EditContext in the same way as in the previous example, with the addition of a private field for an EventHandler<FieldChangedEventArgs>:

EventHandler<FieldChangedEventArgs>? nameChanged;

The nameChanged event handler is defined in OnInititialized and added to the EditContext OnFieldChanged property:

nameChanged = async (_e=>
{
    if (e.FieldIdentifier.FieldName == nameof(Account.Username))
    {
        modelState.Clear(e.FieldIdentifier);
        var valid = await http.GetFromJsonAsync<bool>($"/checkusername?username={Account.Username}");
        if (!valid)
        {
            modelState.Add(editContext.Field(nameof(Account.Username)), "You must choose another user name");
        }
        editContext.NotifyValidationStateChanged();
    }
};
editContext.OnFieldChanged += nameChanged;

This time, we don't have to update the value of the bound field because we haven't taken responsibility for the onchange event of the input component. Otherwise, the code is similar to the previous example. The EditForm looks like this. Notice that using this approach, we can leverage two-way binding using @bind-Value. Note also that just from looking at the EditForm, it's not clear that anything happens when the username changes:

<EditForm EditContext="editContext">
    <label class="form-label">Enter a user name</label>
    <InputText class="form-control" @bind-Value="Account.Username" />
    <ValidationMessage For="() => Account.Username" />
</EditForm>

So up until .NET 7, if you want to run async code onchange, you usually have to lose two-way binding, or hook into other events. A new feature was added to Blazor .NET 7 that enables you to specifiy callbacks to be executed after onchange has executed. Unfortunately, this was not properly implemented in RTM and is only partially working in 7.0.1. You need to install the 7.0.101 version of the .NET SDK and ensure that your VS version is at least 17.4.3. Even then you may see an error when you attempt to use the new feature:

cannot convert from 'method group' to 'Action'

You should still be able to build and run your app regardless. If you still have issues, refer to the advice here.  Assuming that you are able to install the necessary bits or are reading this long after all issues have been resolved, here's how to use the new feature.

@bind:after modifier

Here's the original HTML Element example, with full two-way binding (@bind-value) and a new @bind:after modifier, taking our existing asynchronous method as a parameter:

<label class="form-label">Enter a user name</label>
<input class="form-control" @bind="username" @bind:after="CheckUsername" />
<span class="d-block text-danger">@message</span>

And here's the revised CheckUsername method. Ths time, we don't have to acquire the input value from the ChangeEventArgs object, neither do we have to manually update the bound value. We can work with the existing bound value throughout. It is a lot cleaner and less error-prone.

async Task CheckUsername()
{
    message = string.Empty;
    if (username is not null)
    {
        var valid = await http.GetFromJsonAsync<bool>($"/checkusername?username={username}");
        if (!valid)
        {
            message = "You must choose another user name";
        }
    }
}

For completeness, here's the EditForm version where the directive to use is @bind-Value:after:

<EditForm EditContext="editContext">
    <label class="form-label">Enter a user name</label>
    <InputText class="form-control" 
        @bind-Value="Account.Username" 
        @bind-Value:after="CheckUsername" />
    <ValidationMessage For="() => Account.Username" />
</EditForm>

Once again, we don't have to take responsibility for updating the bound value, or for identifying which field fired the OnFieldChanged event. The development experience is a lot smoother.

async Task CheckUsername()
{
    if (!string.IsNullOrWhiteSpace(Account.Username))
    {
        modelState.Clear(editContext.Field(nameof(Account.Username)));
        var valid = await http.GetFromJsonAsync<bool>($"/checkusername?username={Account.Username}");
        if (!valid)
        {
            modelState.Add(editContext.Field(nameof(Account.Username)), "You must choose another user name");
        }
        editContext.NotifyValidationStateChanged();
    }
}

Summary

This article presents a number of ways in which you can execute async operations when a bound value changes in Blazor. Most often, you have to lose two-way binding in the process. However, with updates to .NET 7, you can now use the @bind:after modifier to solve the problem and keep two-way binding.