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