Blazor from Scratch: Chapter 4 — Data Binding and Events
Welcome to Chapter 4 of Blazor from Scratch. In Chapter 3 we split a growing UI into components. Now we need a predictable way for those components to read state, accept user input, and tell a parent that something happened.
This chapter is for .NET developers who can read a Razor component but still find it unclear why an input updates on blur, how to update on every keystroke, or when a child component should raise an event. The promise is small and practical: by the end, you will have a binding lab that makes each interaction visible and gives you a way to verify it.
The sample targets a .NET 10 Blazor Web App using Interactive Server. It covers browser element binding, the @bind:event, @bind:after, @bind:get, and @bind:set modifiers, ordinary event handlers, and a typed EventCallback. Forms validation, API calls, JavaScript interop, and authentication are intentionally out of scope for this chapter.
The direction of data flow
Start with the simplest case: one-way rendering.
<p>Hello, @displayName.</p>
The component owns displayName, and the current value flows into the rendered markup. Nothing in that markup changes the field.
Binding adds a path back from an interactive element:
<input @bind="displayName" />
Conceptually, Blazor renders the current value and registers a change handler that writes the new value back. The @bind directive also takes care of converting the browser’s value to the type of the bound expression. It is more than string interpolation, and it is why binding is preferable to manually wiring a value and an event when you want the element and .NET state to stay synchronized.
For a text input, the default event is onchange. The field normally updates when the element’s value changes and the element loses focus. If the UI should react to every edit instead, select the DOM event explicitly:
<input @bind="displayName" @bind:event="oninput" />
That one decision affects both behavior and resource usage. oninput is useful for a live preview, but it can send an event for every keystroke in an Interactive Server component.
Version and render-mode note
The code in this post was compiled in a clean temporary project with the .NET SDK 10.0.400 and net10.0. The @bind:get/@bind:set and @bind:after modifiers are not exclusive to .NET 10; the current ASP.NET Core documentation places them in the .NET 7 and later guidance. The explicit target here prevents the generated template and the examples from silently using a different framework.
The current Blazor Web App template can render a page as static server-rendered HTML. Static HTML is useful, but it does not execute the event handlers in this sample. @rendermode InteractiveServer makes this page interactive, while the --interactivity Server option in the project command configures the server-side services and endpoints.
Build the binding lab
Create a fresh project, or use the app from the previous chapter:
dotnet new blazor --framework net10.0 --interactivity Server --output BindingLab
cd BindingLab
The template places components in namespaces based on the project name. Because the reusable component below lives in Components/Common, add this line to Components/_Imports.razor if it is not already present:
@using BindingLab.Components.Common
Replace BindingLab with your project’s root namespace if you chose a different name.
Bind an input, a select, and a checkbox
Create Components/Pages/BindingLab.razor:
@page "/binding-lab"
@rendermode InteractiveServer
<PageTitle>Binding lab</PageTitle>
<h1>Binding lab</h1>
<p>Change the controls and watch the preview update.</p>
<section>
<h2>Display name</h2>
<label for="display-name">Name</label>
<input id="display-name"
@bind:get="displayName"
@bind:set="SetDisplayName"
@bind:event="oninput" />
<p>Name in state: <strong>@displayName</strong></p>
</section>
<section>
<h2>Theme</h2>
<label for="theme">Theme</label>
<select id="theme" @bind="theme" @bind:after="ThemeChanged">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<p>@previewNote</p>
</section>
<section>
<label>
<input type="checkbox" @bind="compactMode" />
Compact spacing
</label>
<p>Spacing: @(compactMode ? "compact" : "comfortable")</p>
</section>
<PreferenceActions DisplayName="@displayName"
Theme="@theme"
CompactMode="@compactMode"
OnSaved="HandleSaved" />
<p role="status" aria-live="polite">@status</p>
@code {
private string displayName = "Ada";
private string theme = "light";
private bool compactMode;
private string previewNote = "Choose a theme.";
private string status = "Nothing saved yet.";
private int savedCount;
private void SetDisplayName(string? value)
{
var candidate = value ?? string.Empty;
displayName = candidate.Length <= 40 ? candidate : candidate[..40];
}
private void ThemeChanged()
{
previewNote = $"Preview uses the {theme} theme.";
}
private Task HandleSaved(string summary)
{
savedCount++;
status = $"Saved #{savedCount}: {summary}.";
return Task.CompletedTask;
}
}
There are three different binding choices in this page:
- The name uses
@bind:getand@bind:setwithoninput, so the setter sees each edit and can enforce a 40-character limit before the value is rendered back. - The theme uses ordinary
@bind, so the select uses its defaultonchangebehavior.@bind:afterupdates the local preview afterthemehas been assigned. - The checkbox binds a Boolean. Blazor maps it to the checkbox’s checked state rather than treating the word
trueorfalseas text.
The status paragraph uses role="status" and aria-live="polite" so a screen reader can announce a save result without stealing focus. Binding is not only about getting a value into a field; it is also about making state changes understandable to the user.
Raise a typed event from a child
Create Components/Common/PreferenceActions.razor:
<div class="actions">
<button type="button" @onclick="NotifySaved">@Label</button>
</div>
@code {
[Parameter] public string DisplayName { get; set; } = string.Empty;
[Parameter] public string Theme { get; set; } = string.Empty;
[Parameter] public bool CompactMode { get; set; }
[Parameter] public string Label { get; set; } = "Save preferences";
[Parameter] public EventCallback<string> OnSaved { get; set; }
private async Task NotifySaved()
{
var density = CompactMode ? "compact" : "comfortable";
var summary = $"{DisplayName} / {Theme} / {density}";
await OnSaved.InvokeAsync(summary);
}
}
The child receives a snapshot of the values it needs to display and emits a string when its button is clicked. The parent owns savedCount and status, so the child does not mutate parent state or write to its own parameters. EventCallback<string> makes the payload explicit; a caller cannot accidentally connect a callback that expects an unrelated type.
The NotifySaved method is an ordinary DOM event handler because it is attached to @onclick. It then invokes the component callback with InvokeAsync. The parent does not need to call StateHasChanged after HandleSaved; Blazor schedules a render after an event handler and after a component callback.
Run the app:
dotnet run
Open /binding-lab at the address printed by the command.
The binding modifiers in context
The modifiers are small, but each one answers a different question.
@bind:event: when should the value change?
Use the default @bind when changing a value on onchange is sufficient:
<select @bind="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
Use @bind:event="oninput" when the view must respond while a text value is being edited:
<input @bind="filterText" @bind:event="oninput" />
<p>Filtering for: @filterText</p>
Do not add a second, competing binding path just because the default timing is not right. Change the event used by @bind, or choose manual event handling when you need complete control over the element.
@bind:after: what should happen after assignment?
@bind:after runs after the bound value has been assigned synchronously:
<select @bind="theme" @bind:after="ThemeChanged">
...
</select>
That ordering matters. ThemeChanged can read the new theme, which is why the sample can update the preview without duplicating the select’s assignment logic.
The delegate passed to @bind:after can return Action-style synchronous work or Task for asynchronous work. It is not an EventCallback parameter. If a child component exposes an EventCallback, invoke that callback from a normal event handler as PreferenceActions does instead.
An asynchronous after method is a useful place for work that logically follows a local value change, such as refreshing local results. It is not a free debounce mechanism. With oninput, it can still start once per keystroke, so remote calls need an explicit cancellation or debounce policy.
@bind:get and @bind:set: can I inspect or change the value?
The :get and :set modifiers are a pair:
<input @bind:get="displayName"
@bind:set="SetDisplayName"
@bind:event="oninput" />
The getter supplies the value rendered into the element. The setter receives the new value before Blazor applies the bound value back to the element. In the sample, SetDisplayName clamps the value and the next render keeps the input and the field aligned.
This is safer than the older pattern of writing value="@displayName" and handling @oninput yourself when the handler can transform the value. A manual handler can change the C# field while the browser element continues to display a different value. :get/:set expresses both directions to Blazor.
The same idea applies to a component parameter. A component with a Value parameter and a matching ValueChanged callback can be used with:
<MyInput @bind-Value="displayName" />
The binding convention connects the parent’s expression to Value and connects the update path to ValueChanged. Keep the parameter as an auto-property in the child; perform transformations in a separate setter method or in the component’s lifecycle rather than creating side effects in the parameter setter.
Events without binding
Binding is not the right abstraction for every event. Use a normal event handler when you need the event itself, not a synchronized value:
<button type="button" @onclick="Increment">Clicked @clickCount times</button>
@code {
private int clickCount;
private void Increment()
{
clickCount++;
}
}
When an event supplies useful details, accept its event-argument type:
@using Microsoft.AspNetCore.Components
<input @onchange="ReadValue" />
<p>Received: @lastValue</p>
@code {
private string lastValue = string.Empty;
private void ReadValue(ChangeEventArgs args)
{
lastValue = args.Value?.ToString() ?? string.Empty;
}
}
For asynchronous handlers, return Task (or ValueTask) rather than async void. Blazor can track the returned task, render after the handler completes, and report exceptions through its normal error handling. You also do not need to call StateHasChanged for an ordinary component event handler.
Verify each interaction
Treat the page as a small experiment rather than assuming that the directives did what you intended:
- Live name binding: focus the name input and type
Grace. TheName in statevalue should follow each character without waiting for focus to leave the input. - Setter boundary: paste a string longer than 40 characters into the name input. The state text and the input should settle on the same first 40 characters.
- Post-bind work: change the theme. The preview should mention the newly selected theme, demonstrating that
ThemeChangedran after assignment. - Boolean binding: toggle compact spacing. Both the text and the value passed to the child should change between
compactandcomfortable. - Callback direction: click Save preferences twice. The parent-owned status should show
Saved #1and thenSaved #2; the child should not need access tosavedCount. - Static versus interactive rendering: remove
@rendermode InteractiveServer, rebuild, and load the page if you want to see the boundary. The initial HTML can still render, but the event handlers will not run until an interactive render mode is applied.
The first two checks are deliberately different. @bind:event controls when the setter is called, while @bind:set controls what value the component accepts. A page can have live input without accepting every input value unchanged.
Common failure modes
The page renders but clicks do nothing
In a Blazor Web App, rendering HTML and running component events are separate concerns. Make sure the app registers interactive server components and maps the interactive server render mode. The template command above does that; @rendermode InteractiveServer opts this page into it.
A value updates only after leaving the field
That is the expected default for @bind on a text input because it uses onchange. Add @bind:event="oninput" only when live updates are useful. For a search box that calls a service, combine live binding with cancellation or debouncing instead of making a request for every key.
C# state and the element value drift apart
This commonly happens with a manual value attribute and an @oninput handler that normalizes the field. Use @bind:get/@bind:set when the setter can reject or transform input. Blazor then knows that the accepted value must be rendered back into the element.
@bind:after rejects the callback
@bind:after expects a method returning synchronous or asynchronous work. It does not accept an EventCallback parameter. Keep @bind:after for post-binding work and use EventCallback for a child-to-parent component event.
An asynchronous handler loses exceptions
An async void handler gives Blazor no task to observe. Change it to async Task, await the work, and decide how the UI should represent loading and failure. The sample stays local and synchronous on purpose; a network-backed version needs those states.
Security, accessibility, and cost
Binding is a UI mechanism, not a security boundary.
- Razor-encoded output is appropriate for the name and status in this sample. Do not turn user input into
MarkupStringjust to make a preview look richer. - A callback from a child does not authorize an operation. Recheck authorization and validate the data at the server or API boundary before persisting anything.
- Interactive Server keeps the component execution on the server and sends events over the interactive connection. Do not place secrets in component fields that are rendered to the browser, and do not treat a hidden control as protection against a crafted request.
- The
aria-livestatus gives save feedback without requiring focus movement. Labels are associated with the text input and select; the checkbox has a visible label. oninputproduces more event traffic and server work thanonchange. Use it for a clearly valuable live response, keep local work cheap, and prefer a deliberate debounce or a less chatty event for remote work. Interactive WebAssembly can change the round-trip trade-off, but it has a different download and deployment profile.
There is no universally correct binding event. Choose the least frequent event that still makes the UI feel correct, then verify the behavior with a real interaction.
Primary documentation
These are the current Microsoft sources used for the version-sensitive details in this chapter:
Up next
Chapter 5 will build on this event direction with component communication: parameters, typed EventCallback, cascading values, and the point where shared state becomes a better boundary than passing one more parameter.