Updated on
On a plain <select>, @bind and @onchange cannot coexist. The compiler reports the onchange attribute as used twice. On an InputSelect, the same pair compiles without complaint, which is worse: the component captures our handler and then overwrites it with its own binder, so it never runs.
The fix is the same shape either way. To react once the value has already been bound, use @bind:after (or @bind-Value:after on InputSelect). To handle the change without binding at all, drop @bind and use @onchange alone. On an InputSelect that means supplying Value, ValueChanged, and ValueExpression by hand.
Both <select> and InputSelect are Blazor components, and the mechanism underneath both is data binding in Blazor.
@onchange vs @bind in Blazor
Our application is a simple burger configurator where we choose the topping from a dropdown menu. We will create a new Blazor WebAssembly application with Visual Studio Project Wizard or use the terminal dotnet new blazorwasm command.
In the code section of our index.razor file we add:
@code
{
public record Topping(int Id, string Name);
public string? selectedTopping;
public List<Topping> toppings = new List<Topping>()
{
new Topping(1, "No Topping"),
new Topping(2, "Cheese"),
new Topping(3, "Onions"),
new Topping(4, "Pickles"),
new Topping(5, "Avocado"),
new Topping(6, "Lettuce")
};
}
We create a Topping record type and initialize several Topping records with Id and Name. We also declare a string variable selectedTopping.
In the body of the index.razor, we add:
@page "/"
<PageTitle>Burger Configurator</PageTitle>
<h1>Burger Configurator</h1>
<p>Free topping: @selectedTopping</p>
<p>Please choose your topping from the list below:</p>
<span>Free topping:</span>
<select class="select-element" @bind="selectedTopping">
@foreach (var topping in toppings)
{
<option value="@topping.Name">@topping.Name</option>
}
</select>
To use onchange event with select dropdown, we add a <select> element with a foreach loop that goes through our toppings and displays it as a dropdown in our application UI. Notice that in the <select> element, we use @bind to bind the selected value in the dropdown with our variable @selectedTopping.
When we run the application and select a topping, the topping appears in the line:
<p>Free topping: @selectedTopping</p>
It seems that there is an @onchange event present even though we didn’t declare it in our <select> element.
It happens because @bind has @onchange event registered which handles changes, so our line:
<select class="select-element" @bind="selectedTopping">
is equal to:
<select class="select-element" value="@selectedTopping" @onchange="@((ChangeEventArgs e) => selectedTopping = e.Value.ToString())">
Our <select> element has an @onchange event attribute with lambda expression where it assigns the selected value to the @selectedTopping variable after the change event fires.
The @bind is a good choice for works storing the value in the variable after the change, but what if we need to perform additional computations based on the selected value?
Let’s see how we use the @onchange event to calculate the cost of a burger based on the topping we select.
How Do We Use @onchange Without @bind?
Firstly, let’s change our Topping record:
@code
{
public record Topping(int Id, string Name, double Price);
public string? selectedTopping;
public string? selectedSecondTopping;
public List<Topping> toppings = new List<Topping>()
{
new Topping(1, "No Topping", 0),
new Topping(2, "Cheese", 2.4),
new Topping(3, "Onions", 0.7),
new Topping(4, "Pickles", 1.3),
new Topping(5, "Avocado", 4.6),
new Topping(6, "Lettuce", 1.1)
};
}
In the Topping record declaration, we add a double type Price and add prices for each topping when we initialize the toppings list, we also add a selectedSecondTopping string variable.
We add a Price preceded by + and $ signs in our foreach loop. This way, we can see the $ amount each topping adds when we select it:
@foreach (var topping in toppings)
{
<option value="@topping.Name">@topping.Name [email protected]</option>
}
Let’s add a method we will use in our @onchange event:
@code
{
public record Topping(int Id, string Name, double Price);
public string? selectedTopping;
public string? selectedSecondTopping;
public static double baseBurgerCost = 5.4;
public double totalCost = baseBurgerCost;
public List<Topping> toppings = new List<Topping>()
{
new Topping(1, "No Topping", 0),
new Topping(2, "Cheese", 2.4),
new Topping(3, "Onions", 0.7),
new Topping(4, "Pickles", 1.3),
new Topping(5, "Avocado", 4.6),
new Topping(6, "Lettuce", 1.1)
};
public void HandleChange(ChangeEventArgs args)
{
@foreach(var topping in toppings)
{
if(topping.Id == int.Parse(args.Value.ToString()))
{
selectedSecondTopping = topping.Name;
totalCost = Math.Round(baseBurgerCost + topping.Price, 2);
}
}
}
}
First, we add a baseBurgerCost variable which is a cost without a topping, and a totalCost variable initialized with the baseBurgerCost value.
Then, we create a HandleChange() method that executes when the @onchange event fires. Inside, we have a foreach loop that goes through toppings and checks whether the id of the currently selected topping equals a topping from the List. When it finds a match, it assigns a name to the selectedSecondTopping variable, calculates the total cost of the burger, and sets it to the totalCost variable.
We also need to adapt our page body:
@page "/"
<PageTitle>Burger Configurator</PageTitle>
<h1>Burger Configurator</h1>
<p>Burger without topping: $@baseBurgerCost</p>
<p>Free topping: @selectedTopping</p>
<p>Total Cost: $@totalCost</p>
<p>Please choose your toppings from the list below:</p>
<span>Free topping:</span>
<select class="select-element" @bind="selectedTopping">
@foreach (var topping in toppings)
{
<option value="@topping.Name">@topping.Name</option>
}
</select>
<span>Second topping:</span>
<select class="select-element" @onchange="@HandleChange">
@foreach (var topping in toppings)
{
<option value="@topping.Id">@topping.Name [email protected]</option>
}
</select>
We add baseBurgerCost and totalCost variable outputs to see the results.
Then, we set the topping id as a value of the <option> element and assign the HandleChange() method to the @onchange attribute.
Our entire logic for total cost calculation is now inside the HandleChange() method which executes when the @onchange event fires.
Can We Use @bind With @onchange?
Let’s add @bind to our <select> element:
<select class="select-element" @bind="@selectedSecondTopping" @onchange="@HandleChange">
@foreach (var topping in toppings)
{
<option value="@topping.Id">@topping.Name [email protected]</option>
}
</select>
The compiler will immediately complain and show us an error on the @onchange attribute. The reason is that @bind uses @onchange internally, so it is not possible to use @bind and @onchange on the same element, as it throws an error:
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.
What Happens If We Use @bind-Value and @onchange Together on InputSelect?
InputSelect is the EditForm version of a dropdown, and it binds with @bind-Value rather than @bind. What happens when we add @onchange is not what the plain <select> does.
A plain <select> refuses to build. @bind already registers a change handler, so a second one is a duplicate attribute and the compiler says so.
InputSelect builds without complaint. It inherits AdditionalAttributes, a parameter that captures every attribute the component does not declare, so @onchange lands there instead of erroring. Then the component renders: it splats those captured attributes first and writes its own onchange binder afterwards. The later write wins, and our handler is discarded before the browser ever sees it.
That is the trap. There is no error, no warning, and no runtime exception. The dropdown simply changes value and our method never runs.
There is a second reason not to fight it. InputSelect reports changes to the EditContext so validation runs, and bypassing the binding to attach a raw handler skips that.
InputSelect lives inside an EditForm, and if that pairing is new, forms and form validation in Blazor WebAssembly covers the surrounding pieces this section assumes.
How Do We Handle Change Events on a Blazor InputSelect?
There are two supported patterns, and the choice is whether we still want the value bound.
Keeping the binding is almost always right, and @bind-Value:after runs our method once the value has been written to the model:
<InputSelect @bind-Value="order.ToppingId" @bind-Value:after="RecalculateTotal">
@foreach (var topping in toppings)
{
<option value="@topping.Id">@topping.Name</option>
}
</InputSelect>
RecalculateTotal takes no parameters and reads the model, because by the time it runs the new value is already there. It may return Task, so awaiting a price lookup works without extra ceremony.
Replacing the binding is the other option, and it means supplying by hand what @bind-Value was generating: a Value parameter, a ValueChanged callback, and a ValueExpression. Omitting ValueExpression compiles and then breaks validation at runtime, which is the usual reason this route disappoints.
The callback also becomes ours to honour. ValueChanged is an EventCallback<T> that we must invoke ourselves for the parent to see the new value, and forgetting that leaves a dropdown that visibly changes while the model never does.
Prefer the first pattern unless the component genuinely must not own the value.
| What we need | Plain <select> | InputSelect inside EditForm |
|---|---|---|
| Store the value only | @bind="value" | @bind-Value="model.Value" |
| Run code after the value is stored | @bind:after="Handler" | @bind-Value:after="Handler" |
| Handle the change instead of binding | @onchange="Handler" (no @bind) | ValueChanged + Value + ValueExpression |
Both binding and @onchange on one element | Compiler error: onchange used twice | Compiles, then silently discards our handler |
| Handler receives | ChangeEventArgs with Value as object? | The already-typed value |
| Async handler | Yes, @bind:after accepts a Task method | Yes, @bind-Value:after accepts a Task method |
| Validation integration | None | Yes, participates in EditContext |
The ordering is documented, not incidental: “An assigned C# delegate isn’t executed until the bound value is assigned synchronously” (ASP.NET Core Blazor data binding).
Getting the ValueExpression parameter right is also what keeps custom validation in Blazor WebAssembly working once the field is wired up.
How Do We Run Code After @bind Updates the Value?
@bind:after has been the standard approach since .NET 7 and is current on .NET 10. The assigned method won’t execute until the synchronous bind for the value is complete. The method we pass to @bind:after needs to return either Task or Action. In this scenario, the <select> element has a @bind attribute that binds the value of the selected element. Then, the @bind:after is executed asynchronously after @bind is bound to the selected element’s value.
On an InputSelect, the equivalent is @bind-Value:after — see “How Do We Handle Change Events on a Blazor InputSelect?” above.
One constraint comes from the docs word for word: “Using an event callback parameter (EventCallback/EventCallback<T>) with @bind:after isn’t supported.” Pass a method returning Action or Task instead (ASP.NET Core Blazor data binding).
Let’s see a @bind:after example with a second topping:
@page "/"
<PageTitle>Burger Configurator</PageTitle>
<h1>Burger Configurator</h1>
<p>Burger without topping: $@baseBurgerCost</p>
<p>Free topping: @selectedTopping</p>
<p>Total Cost: $@totalCost</p>
<p>Grand Total: $@grandTotal</p>
<p>Please choose your toppings from the list below:</p>
<span>Free topping:</span>
<select class="select-element" @bind="selectedTopping">
@foreach (var topping in toppings)
{
<option value="@topping.Name">@topping.Name</option>
}
</select>
<span>Second topping:</span>
<select class="select-element" @onchange="@HandleChange">
@foreach (var topping in toppings)
{
<option value="@topping.Id">@topping.Name [email protected]</option>
}
</select>
<span>Third topping:</span>
<select class="select-element" @bind="selectedThirdTopping" @bind:after="CalculateGrandTotal">
@foreach (var topping in toppings)
{
<option value="@topping.Id">@topping.Name [email protected]</option>
}
</select>
We add another <select> element along with a @grandTotal output.
Next, we add the following variables and a method to our @code section:
public string? selectedThirdTopping;
public double grandTotal;
public async Task CalculateGrandTotal()
{
await Task.Delay(2000);
foreach (var topping in toppings)
{
if (topping.Id == int.Parse(selectedThirdTopping))
{
grandTotal = Math.Round(totalCost + topping.Price, 2);
}
}
}
With Task.Delay() we simulate an asynchronous request that will fetch data and return a result after a certain amount of time. The important thing to note here is that our asynchronous logic will execute only after the value has been bound to the variable set to the @bind attribute. This way we can use the selected value to make an asynchronous request and return data.
Conclusion
User input is a core part of any user-facing application. We need to get a currently selected value and be able to apply additional logic after the change occurs.
We explored the use of onchange event with the select dropdown in a Blazor application by using the @bind to assign the current value to the variable as it already internally contains an onchange event.
When we have our logic in a separate method, we can use the onchange attribute to execute our code when the change occurs. We can use the @bind:after attribute for asynchronous methods, as it will run after the new value is bound to the assigned variable.
The ChangeEventArgs shape used throughout this article is part of Blazor’s broader custom event arguments in Blazor model.
Tested with .NET 10.0.10.
