Data Binding
Here's the one idea that makes Blazor feel alive instead of static: binding ties your markup to your C# state. You don't manually grab an element and shove text into it the way you would with document.getElementById in JavaScript. You declare a relationship - "this bit of UI reflects that field" - and Blazor keeps them in sync.
That relationship runs in one of two directions:
- One-way (
@field): your C# state flows into the markup. When the field changes and the component re-renders, the DOM updates. The data travels state → UI. - Two-way (
@bind): your C# state and a form input stay locked together in both directions. The user types, the field updates; your code changes the field, the input updates. Data travels state ↔ UI.
Hold that picture - one-way is a read-out, two-way is a handshake - and everything below is just syntax for those two cases. We'll build it on the running products UI from the earlier phases.
📝 You already met one-way binding in Components & Razor without us naming it. Every time you wrote
@somethingin markup, that was one-way binding. This phase names it, then adds the two-way kind.
One-way binding: state into markup
When you drop @expression into your markup, Blazor evaluates that C# and renders the result. If the value changes later (and the component re-renders), the rendered output changes with it. The data only ever flows one way: from your field into the page.
<h2>@ProductName</h2>
<p>Price: $@Price</p>
<p>In stock: @(InStock ? "yes" : "no")</p>
@code {
private string ProductName = "Mechanical Keyboard";
private decimal Price = 89.99m;
private bool InStock = true;
}
What just happened: the three @ expressions read straight out of the @code fields and render their current values. Notice @(InStock ? ... : ...) - when the expression is more than a simple name, wrap it in parentheses so Blazor knows where the C# ends and markup resumes. This is a one-way relationship: the markup mirrors the fields, but nothing in the markup can change them. For that, you need two-way binding.
Two-way binding with @bind
A read-out isn't enough for a form. When the user types into a search box or edits a price, you want that typed value to land back in your C# field. That's @bind.
@bind ties a form element's value to a C# field in both directions: change the field in code and the input updates; type in the input and the field updates. Let's wire up a search box that filters the product list.
<input @bind="filter" placeholder="Search products..." />
<p>Filtering by: <strong>@filter</strong></p>
<ul>
@foreach (var p in products.Where(p => p.Contains(filter, StringComparison.OrdinalIgnoreCase)))
{
<li>@p</li>
}
</ul>
@code {
private string filter = "";
private List<string> products = new()
{
"Mechanical Keyboard", "Wireless Mouse", "USB-C Hub", "Monitor Arm"
};
}
What just happened: @bind="filter" connects the text box to the filter field. When filter changes, the @foreach re-runs and the list narrows. The <strong>@filter</strong> next to it is one-way binding reading the same field - one field, two roles, side by side.
@bind isn't only for text boxes. It adapts to the element it's on:
<select @bind="category">
<option value="all">All</option>
<option value="input">Input devices</option>
<option value="display">Displays</option>
</select>
<label>
<input type="checkbox" @bind="inStockOnly" />
In stock only
</label>
<p>Category: @category · In-stock filter: @inStockOnly</p>
@code {
private string category = "all";
private bool inStockOnly = false;
}
What just happened: on a <select>, @bind reads and writes the chosen <option>'s value. On <input type="checkbox">, it binds to a bool - checked is true, unchecked false. Blazor picks the right HTML attribute and conversion per element, so you write the same @bind everywhere.
💡 The timing detail that trips people up: by default
@bindon a text input syncs ononchange- fires when the input loses focus, not on every keystroke. In the search example above,filteronly updates after you click away or press Tab, often not what you want for a live search. The next section fixes it.
Controlling when and how it syncs
@bind has a few companion directives that tune its behavior.
@bind:event changes the event that triggers the sync. Switch it to oninput and the field updates on every keystroke - perfect for a live filter:
<input @bind="filter" @bind:event="oninput" placeholder="Search products..." />
<p>Showing results for: <strong>@filter</strong></p>
@code {
private string filter = "";
}
What just happened: with @bind:event="oninput", filter updates as the user types each character, so the filtered list reacts instantly instead of waiting for the box to lose focus. The default onchange is fine for a price field you only care about once editing is done; oninput is for anything that should feel live.
@bind:format controls how a value is rendered into the input - most commonly for dates:
<input type="date" @bind="restockDate" @bind:format="yyyy-MM-dd" />
<p>Restocks on: @restockDate.ToShortDateString()</p>
@code {
private DateTime restockDate = DateTime.Today;
}
What just happened: @bind:format="yyyy-MM-dd" tells Blazor how to format the DateTime when writing it into the input's value. Without a matching format, an <input type="date"> may refuse to display the value, since the browser expects exactly yyyy-MM-dd.
@bind:after runs a method after the bound field has been updated - the right place to react to a change (recalculate, log, call an API):
<input @bind="filter" @bind:event="oninput" @bind:after="OnFilterChanged" />
<p>@status</p>
@code {
private string filter = "";
private string status = "";
private void OnFilterChanged()
{
status = string.IsNullOrWhiteSpace(filter)
? "Showing all products"
: $"Searching for \"{filter}\"";
}
}
What just happened: each time the bind updates filter, Blazor calls OnFilterChanged, which reads the freshly-updated value and sets status. The key word is after - by the time your method runs, filter already holds the new value.
⚠️ This one bites everyone:
@bind="x"is shorthand forvalue="@x"plus an@onchangehandler that assigns the typed value back tox. Because@bindalready owns that handler, you cannot also add your own@onchangeto the same element - Blazor will error. Use@bind:after(shown above), or drop@bindentirely and writevalue="@x"with your own@onchangehandler doing both the assignment and your logic. Never both on the same element.
When does the UI actually update?
Binding handles the sync, but re-rendering is what makes the change visible. When a bound field changes because of the UI (a keystroke, a checkbox click), Blazor re-renders that component automatically - that's why the filtered list updates without you lifting a finger.
💡 The exception is state you change from your own code outside a UI event - a background timer ticking, a value updated inside an
asynccallback. There, Blazor may not know it needs to re-render, and you nudge it withStateHasChanged(). Covered in Events & the Component Lifecycle. For now: UI-driven changes render themselves; code-driven changes sometimes need a tap on the shoulder.
Recap
- Binding ties markup to state. One-way (
@field) flows state → UI; two-way (@bind) keeps a form input and a C# field in sync in both directions. - One-way is anything you write as
@expressionin markup - a read-out of your fields. Wrap non-trivial expressions in@( ... ). @bindworks on text inputs,<select>,<textarea>, and checkboxes (bound to abool), picking the right attribute and conversion for each. By default it syncs ononchange- when the element loses focus.- Tune the sync with
@bind:event="oninput"(every keystroke),@bind:format(e.g. date formatting), and@bind:after(run a method once the field has updated). @bindis sugar forvalue+ an@onchangehandler, so you can't add a second manual@onchangeto the same element - use@bind:afteror hand-roll value + handler instead.- UI-driven changes re-render automatically; changes from your own code may need
StateHasChanged()(Phase 4).
Quick check
[
{
"q": "By default, when does @bind on a text <input> sync the field?",
"choices": ["On every keystroke", "When the input loses focus (the onchange event)", "Only when the form is submitted", "Once when the component first renders"],
"answer": 1,
"explain": "Default @bind syncs on the onchange event, which fires when the input loses focus. Use @bind:event=\"oninput\" to sync on every keystroke."
},
{
"q": "Why can't you add your own @onchange handler to an element that already uses @bind?",
"choices": ["@bind only works on read-only elements", "@bind is shorthand for value + an @onchange handler, so it already owns that event", "@onchange is not a valid Blazor event", "You can, but only if the field is a string"],
"answer": 1,
"explain": "@bind=\"x\" expands to value=\"@x\" plus an @onchange handler that writes back. Adding another @onchange conflicts; use @bind:after instead."
},
{
"q": "Which directive runs a method right after a bound field has been updated?",
"choices": ["@bind:format", "@bind:event", "@bind:after", "@onchange"],
"answer": 2,
"explain": "@bind:after runs your method once the bind has assigned the new value, so the field already holds the updated value when it runs."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. By default, when does @bind on a text <input> sync the field?
2. Why can't you add your own @onchange handler to an element that already uses @bind?
3. Which directive runs a method right after a bound field has been updated?