Blazor keeps your C# state and the DOM in sync through a process of diffing and reconciliation. Every time StateHasChanged() runs, Blazor compares the new render tree with the previous one and patches only what changed. Data binding is the set of tools that drives data from C# into the DOM (one-way) and from DOM inputs back into C# (two-way).
There are three distinct binding modes, each with a different trigger. Picking the wrong one causes bugs that are surprisingly hard to trace — the UI looks fine until it doesn't.
One-Way Binding — @variable
The simplest form: Blazor evaluates a C# expression and renders its string representation. The DOM updates whenever the component re-renders. The user's input has no path back to C#.
@page "/product"
<h2>@productName</h2>
<p>Price: $@price.ToString("F2")</p>
<p>Status: @(inStock ? "In Stock" : "Out of Stock")</p>
<button @onclick="ChangeName">Change Name</button>
@code {
private string productName = "Blazor Pro Toolkit";
private decimal price = 49.99m;
private bool inStock = true;
private void ChangeName()
{
productName = "Blazor Ultra Toolkit";
// StateHasChanged() is called automatically after an event handler.
}
}
When ChangeName() runs, Blazor re-renders and the <h2> updates. Nothing in the markup ever writes back to productName — that is the definition of one-way binding.
Two-Way Binding with @bind — Fires on Blur
Add an <input> and you usually want changes the user makes to be reflected in C#. @bind handles this:
@page "/profile"
<input @bind="personName" />
<p>Hello, @personName!</p>
@code {
private string personName = "Ada";
}
Critical detail: @bind fires on the change event, which the browser raises when the input loses focus (blur), not on every keystroke. Type into the box, keep focus there, and the <p> will not update. Click away — and it does. This is intentional and correct for most form fields, but it catches developers off guard the first time.
What @bind Compiles To
Understanding the desugaring demystifies all binding behavior:
<!-- What you write -->
<input @bind="personName" />
<!-- What Blazor generates -->
<input value="@personName"
@onchange="@(e => personName = e.Value?.ToString())" />
It is two separate directives: a value attribute (one-way, C# → DOM) and an @onchange handler (DOM → C#). Any behavior you need beyond what @bind provides can be implemented by writing these two lines manually.
Real-Time Binding with @bind:event="oninput"
A search box that updates as the user types requires the oninput event, which fires on every keystroke:
@page "/search"
<input @bind="searchTerm"
@bind:event="oninput"
placeholder="Search products…" />
<p>You typed: @searchTerm</p>
<p>Results: @FilteredCount matching products</p>
@code {
private string searchTerm = "";
private int FilteredCount =>
AllProducts.Count(p =>
p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));
private List<(string Name, decimal Price)> AllProducts = new()
{
("Blazor Toolkit", 49.99m),
("ASP.NET Guide", 29.99m),
("C# Mastery", 39.99m),
("LINQ Workbook", 19.99m),
};
}
Every keystroke triggers the event handler, updates searchTerm, and causes a re-render — so FilteredCount recalculates live.
Binding Beyond Strings
Blazor's binding system performs automatic type coercion. An int, decimal, bool, or DateTime field binds without any conversion code:
@page "/counter"
<p>Count: @count</p>
<button @onclick="Increment">+</button>
<button @onclick="Decrement">-</button>
<p>Direct input:</p>
<input type="number" @bind="count" />
@code {
private int count = 0;
private void Increment() => count++;
private void Decrement() => count--;
}
The numeric input reflects count and writes back on blur. Blazor parses the string from the DOM into an int for you. If parsing fails, the previous value is restored.
Common Mistakes
- Forgetting
@bind:event="oninput"on search boxes. The field binds correctly but feels laggy — updates only happen on blur, not as the user types. Add the event modifier. - Binding to a computed (get-only) property. If the property has no setter, Blazor silently fails to write back. Either add a setter or use an explicit
@onchangehandler. - Omitting
type="checkbox"on a bool binding. Write<input type="checkbox" @bind="isActive" />. Without the type attribute the binding still compiles but the behaviour is undefined.
Key Takeaways
- One-way binding (
@variable) renders C# state into the DOM; it never reads back from it. @binddesugars into a value attribute plus an@onchangehandler — updates fire on blur, not on keypress.- Add
@bind:event="oninput"whenever you need real-time, per-keystroke reactivity such as live search. - Blazor coerces common types automatically —
int,decimal,bool, andDateTimebind without manual parsing. - If a binding appears to do nothing, check that the property has a setter and that you have specified the correct event.