Web UIs with Blazor
Reviewed & published by Brayan K
Blazor lets you build interactive web front-ends in C# instead of JavaScript. By the end of this lesson you'll understand Blazor components in .razor files, Razor syntax, passing data with [Parameter], two-way binding with @bind, event handlers like @onclick, the OnInitializedAsync lifecycle, dependency injection with @inject, routing with @page, the difference between Blazor Server and Blazor WebAssembly, and forms with EditForm validation.
Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Building components in .razor files with Razor syntax
- Passing data into components with [Parameter]
- Two-way binding (@bind) and event handlers (@onclick)
- The component lifecycle and OnInitializedAsync
- Dependency injection with @inject and routing with @page
- Blazor Server vs WebAssembly, and forms with EditForm validation
💡 Real-World Analogy
Think of a web page as a wall built from LEGO bricks. Each Blazor component is a brick — a self-contained piece with its own shape (markup) and studs (parameters) that snap into a parent. A [Parameter] is a stud the parent presses data onto; @bind is a two-way connector so a brick and its data move together; an event like @onclick is a little switch on the brick. You assemble a whole page by clicking small, reusable bricks together — and because they're all the same C# you already know, you don't switch languages to do it.
The mental model: components that re-render on state change
A Blazor app is a tree of components. Each component is a .razor file mixing HTML-like markup with C# in a @code block. The component holds state (fields), and when that state changes — typically from an event handler — Blazor automatically re-renders the component and updates only the parts of the page that changed. You describe what the UI should look like for a given state; Blazor handles the DOM.
- 🧱 Component — a reusable .razor unit of UI plus its logic.
- 🎚️ [Parameter] — data a parent passes into a child component.
- 🔁 @bind — two-way binding between an input and a C# value.
- ⚡ @onclick — an event handler that runs a C# method, after which the component re-renders.
This is the same component-and-state idea behind every modern front-end framework — here it's just C# all the way down.
📊 Razor Directives & Syntax
| Syntax | Means | Example |
|---|---|---|
| @page | Route for the component | @page "/counter" |
| @code | C# logic & state block | @code { ... } |
| [Parameter] | Input from a parent | [Parameter] public string Name |
| @bind | Two-way binding | <input @bind="name" /> |
| @onclick | Click event handler | @onclick="Save" |
| @inject | Dependency injection | @inject HttpClient Http |
In Razor, an @ switches from markup into C#: @name prints a value, and @if/@foreach drive the markup.
1. Components, State, and Events
A Blazor component is a .razor file: HTML-like markup at the top, a @code block of C# below. Fields in @code are the component's state, and you display them in markup with @field. Wire a button to a method with @onclick; when the method changes state, Blazor automatically re-renders — no manual DOM updates. Read the classic counter and note how clicking updates the displayed count.
@* Counter.razor — a Blazor component is HTML markup + C# in one file. *@
@* The @page directive makes it routable at /counter. *@
@page "/counter"
<h3>Counter</h3>
<p>Current count: @currentCount</p>
@* @onclick wires the button to the C# method below. After it runs,
Blazor re-renders the component, so the new count shows on screen. *@
<button @onclick="IncrementCount">Click me</button>
@* The @code block holds the component's C# logic and state. *@
@code {
// A private field is the component's state.
private int currentCount = 0;
private void IncrementCount()
{
currentCount++; // change state...
// ...Blazor automatically re-renders to reflect it. No manual DOM work.
}
}2. Two-Way Binding with @bind
@bind connects an input to a C# field in both directions: the input shows the current value, and the user's edits flow straight back into the field. Add @bind:event="oninput" to update on every keystroke rather than when the box loses focus. Because the field is the single source of truth, anything else that displays it updates live too.
@page "/greet"
<h3>Live Greeting</h3>
@* @bind creates TWO-WAY binding: the input shows 'name', and as the
user types, their edits flow back into the C# 'name' field. *@
<input @bind="name" @bind:event="oninput" placeholder="Your name" />
@* Because the field changed, this line re-renders with the new value. *@
<p>Hello, @name!</p>
@code {
// The single source of truth for both the input and the greeting.
private string name = "";
}Your turn. This toggle just needs its click wired up and the boolean flipped. Fill in the two ___ blanks using the hints.
@page "/toggle"
<h3>Toggle</h3>
@* 🎯 YOUR TURN — fill in the blanks marked with ___ *@
@* 1) Wire this button's click to the Flip method in @code. *@
<button ___="Flip">Flip the switch</button> @* 👉 the click directive: @onclick *@
<p>The light is @(isOn ? "ON" : "OFF").</p>
@code {
private bool isOn = false;
private void Flip()
{
// 2) Toggle the boolean so each click flips ON/OFF.
isOn = ___; // 👉 the negation of isOn: !isOn
}
}3. Reusable Components with [Parameter]
Components become reusable when a parent can pass data into them. Mark a public property with [Parameter] and the parent sets it like an HTML attribute: <GreetingCard Name="Ada" />. This is how you build a small component once and render it many times with different data — cards, list rows, badges, and so on.
@* GreetingCard.razor — a reusable child component. *@
@* A parent passes data IN through [Parameter] properties. *@
<div class="card">
<h4>Hello, @Name!</h4>
<p>You are visitor number @VisitNumber.</p>
</div>
@code {
// [Parameter] exposes these so a parent can set them like attributes.
[Parameter] public string Name { get; set; } = "Guest";
[Parameter] public int VisitNumber { get; set; }
}
@* A parent renders it like this (illustrative):
<GreetingCard Name="Ada" VisitNumber="42" />
which displays: "Hello, Ada!" and "You are visitor number 42." *@Now you try. Finish this reusable price tag by exposing both fields as parameters so a parent can set them. Fill in the two ___ blanks:
@* PriceTag.razor — finish this reusable child component. *@
<span class="tag">@Label: $@Amount</span>
@code {
// 🎯 YOUR TURN — fill in the blanks marked with ___
// 1) Expose 'Label' so a PARENT can set it as an attribute.
[___] public string Label { get; set; } = "Price"; // 👉 the attribute: Parameter
// 2) Expose 'Amount' the same way so the parent can pass a number.
[Parameter] public decimal ___ { get; set; } // 👉 the property name: Amount
}
@* Parent usage: <PriceTag Label="Total" Amount="19.99" /> *@4. Dependency Injection and Lifecycle
Components often need services — an HttpClient to call an API, a custom data service. @inject asks the DI container for one and assigns it to a property. To load data, override OnInitializedAsync, which runs once when the component initializes and lets you await the work; when it finishes, Blazor re-renders with the results. Show a "Loading..." placeholder until the data arrives.
@page "/weather"
@* @inject asks the DI container for a registered service and assigns
it to a property — here an HttpClient for calling an API. *@
@inject HttpClient Http
<h3>Forecast</h3>
@if (forecasts == null)
{
<p>Loading...</p> @* shown until the data arrives *@
}
else
{
<ul>
@foreach (var f in forecasts)
{
<li>@f</li>
}
</ul>
}
@code {
private string[] forecasts;
// OnInitializedAsync runs once when the component initializes —
// the right place to AWAIT data loading.
protected override async Task OnInitializedAsync()
{
// Pretend to fetch from an API; here we just simulate a delay.
await Task.Delay(500);
forecasts = new[] { "Sunny", "Cloudy", "Rainy" };
// When the method completes, Blazor re-renders with the data.
}
}🔎 Deep Dive: Blazor Server vs Blazor WebAssembly
Blazor runs your components in one of two hosting models, and the component code looks almost identical in both:
- Blazor Server — your C# runs on the server. UI events travel to the server over a persistent SignalR connection, and the server sends back a small diff of what changed. Tiny initial download and full server resources, but it needs a live connection and adds network latency per interaction.
- Blazor WebAssembly — a .NET runtime is downloaded and your C# runs in the browser via WebAssembly. After the initial load it works offline and feels instant, at the cost of a larger first download.
// Same component, either host:
// Server -> C# executes on the server, UI diffs over SignalR
// WebAssembly -> C# executes in the browser (WASM), no round-tripsBecause the programming model is shared, you can often start with one model and switch later with minimal changes.
Putting It Together: a Validated Sign-Up Form
Here's a realistic form that ties the lesson together. An EditForm binds to a model; a DataAnnotationsValidator turns [Required] and [EmailAddress] attributes into live validation; InputText uses @bind-Value; and OnValidSubmit only fires once the input is valid. ValidationMessage shows the error next to each field.
@page "/signup"
@* An EditForm bound to a model, validated by data annotations. *@
@using System.ComponentModel.DataAnnotations
<h3>Sign Up</h3>
<EditForm Model="user" OnValidSubmit="HandleSubmit">
@* Turns [Required]/[EmailAddress] attributes into live validation. *@
<DataAnnotationsValidator />
<label>Name:</label>
<InputText @bind-Value="user.Name" />
<ValidationMessage For="@(() => user.Name)" />
<label>Email:</label>
<InputText @bind-Value="user.Email" />
<ValidationMessage For="@(() => user.Email)" />
<button type="submit">Register</button>
</EditForm>
<p>@status</p>
@code {
private Account user = new();
private string status = "";
// OnValidSubmit fires ONLY when validation passes.
private void HandleSubmit() => status = "Welcome, " + user.Name + "!";
public class Account
{
[Required] public string Name { get; set; } = "";
[Required, EmailAddress] public string Email { get; set; } = "";
}
}Validation, binding, and submission are all C# — the same skills from the rest of the course, now driving a web form.
Pro Tips
- 💡 Load data in OnInitializedAsync, not in the constructor — it can await, and Blazor re-renders when it completes.
- 💡 Use @bind:event="oninput" when you want updates on every keystroke instead of on blur.
- 💡 Keep components small and parameter-driven so they're reusable and easy to reason about.
- 💡 Call StateHasChanged() only when state changes outside Blazor's awareness (e.g. a timer callback) — normal event handlers re-render for you.
- 💡 Pick the host by need: Blazor Server for tiny downloads and server access; WebAssembly for offline, low-latency, client-side apps.
Common Errors (and the fix)
- UI doesn't update after changing data: you changed state outside an event handler (e.g. a timer) — call StateHasChanged() so Blazor re-renders.
- "No service for type ... has been registered": you @injected a service that wasn't registered in DI. Register it in Program.cs (e.g. builder.Services.AddScoped<...>()).
- Parameter never gets set: the property isn't public or is missing [Parameter]. Both are required for a parent to pass a value in.
- Click handler doesn't fire: you wrote onclick instead of @onclick, or referenced a method name that doesn't match the one in @code.
- Form submits with invalid data: you used OnSubmit instead of OnValidSubmit, or forgot the <DataAnnotationsValidator /> inside the EditForm.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Make a route | @page "/path" | Top of the .razor file |
| Show a value | @count | @ switches to C# |
| Handle a click | @onclick="DoThing" | Re-renders after |
| Two-way bind | @bind="name" | Input ↔ field |
| Accept input | [Parameter] public ... | From a parent |
| Inject a service | @inject HttpClient Http | From DI |
| Load on init | OnInitializedAsync() | await data here |
| Validated form | <EditForm Model="m"> | + DataAnnotationsValidator |
Frequently Asked Questions
Q: Does Blazor replace JavaScript entirely?
For most UI logic, yes — you write components in C#. You can still call JavaScript when you need a browser API or a JS library (via JS interop), but day-to-day interactivity is C#.
Q: Should I choose Blazor Server or WebAssembly?
Server gives a tiny initial download and direct server access but needs a live SignalR connection. WebAssembly runs in the browser, works offline, and feels instant after a larger first load. The component code is nearly identical either way.
Q: What's the difference between @bind and @onclick?
@bind creates a two-way data link between an input and a C# value. @onclick (and other @on... handlers) runs a C# method in response to an event, after which the component re-renders.
Q: Why use OnInitializedAsync instead of the constructor for loading data?
The constructor can't await, and parameters aren't set yet. OnInitializedAsync runs after the component is set up, can await async calls, and triggers a re-render when the data is ready.
Mini-Challenge: A Live Tip Calculator
No blanks this time — just a brief and an outline. Build a component with bound bill and tipPercent inputs and a computed Tip property that updates live as the user types. For a bonus, also show the grand total. Run it and watch the tip recalculate on every keystroke.
@page "/tip"
<h3>Tip Calculator</h3>
@* 🎯 MINI-CHALLENGE: a live tip calculator component
1. Add two fields in @code: 'bill' (decimal) and 'tipPercent' (int, e.g. 15).
2. Two-way bind a number <input> to each using @bind, so editing them
updates the C# fields.
3. Add a computed property: decimal Tip => bill * tipPercent / 100m;
4. Display the tip with <p>Tip: $@Tip</p> so it updates as the user types.
5. BONUS: also show the grand total (bill + Tip).
✅ Expected: bill 50 and tipPercent 20 shows "Tip: $10" live, updating
the instant either input changes. *@
@code {
// your code here
}🎉 Lesson Complete
- ✅ Blazor builds interactive web UIs in C# using .razor components
- ✅ State lives in @code; changing it triggers an automatic re-render
- ✅ [Parameter] passes data into components; @bind creates two-way bindings
- ✅ @onclick handles events; OnInitializedAsync loads data on init
- ✅ @inject brings in services; @page defines routes
- ✅ Blazor Server runs C# on the server over SignalR; WebAssembly runs it in the browser; EditForm validates forms
- ✅ Congratulations — you've reached the end of the C# course! You can now build console, desktop, game, and web apps in C#.
Practice quiz
What does Blazor let you build that previously required JavaScript?
- Interactive web user interfaces written in C#
- Mobile device drivers
- SQL database engines
- Operating system kernels
Answer: Interactive web user interfaces written in C#. Blazor lets you build interactive web UIs using C# and Razor components instead of writing the UI logic in JavaScript.
What file extension do Blazor components use?
- .xaml
- .cshtml
- .razor
- .blazor
Answer: .razor. Blazor components are written in .razor files, which combine Razor markup (HTML plus C#) with component logic.
Which attribute marks a C# property as a component parameter that a parent can set?
- [Parameter]
- [Inject]
- [Bind]
- [Route]
Answer: [Parameter]. The [Parameter] attribute exposes a public property so a parent component can pass a value into the child, like an attribute in markup.
In Razor markup, how do you wire a button click to a C# method named Increment?
- click={Increment}
- @onclick="Increment"
- bind:click=Increment
- onclick=Increment
Answer: @onclick="Increment". Razor event handlers use the @on prefix; @onclick="Increment" calls the C# method when the button is clicked.
What does @bind do on an input element?
- Sends the value to a database
- Deletes the element
- Adds CSS classes only
- Creates a two-way binding between the input and a C# field/property
Answer: Creates a two-way binding between the input and a C# field/property. @bind sets up two-way data binding: the input shows the C# value and writes user edits back into that field or property.
Which lifecycle method is the right place to load data asynchronously when a component initializes?
- OnInitializedAsync()
- Render()
- Dispose()
- Main()
Answer: OnInitializedAsync(). OnInitializedAsync() runs when the component is first initialized and is the standard place to await data loading such as an API call.
How do you request a service via dependency injection inside a .razor component?
- @page the service
- @inject SomeService Name
- @using the service
- new Service()
Answer: @inject SomeService Name. The @inject directive asks the DI container for a service and assigns it to a property, e.g. @inject HttpClient Http.
What is the key difference between Blazor Server and Blazor WebAssembly?
- Server uses C# while WebAssembly uses Java
- WebAssembly cannot render HTML
- There is no difference
- Server runs C# on the server with UI updates over a SignalR connection; WebAssembly runs the .NET code in the browser
Answer: Server runs C# on the server with UI updates over a SignalR connection; WebAssembly runs the .NET code in the browser. Blazor Server executes component logic on the server and pushes UI diffs to the browser over SignalR; Blazor WebAssembly downloads a .NET runtime and runs the C# directly in the browser.
Which directive at the top of a component defines its route, e.g. /counter?
- @route
- @url
- @page "/counter"
- @map
Answer: @page "/counter". @page "/counter" sets the route template so navigating to /counter renders that routable component.
What does an EditForm with DataAnnotationsValidator provide?
- Automatic database migrations
- Model-based form validation that shows messages when input is invalid
- Image compression
- Server logging only
Answer: Model-based form validation that shows messages when input is invalid. EditForm binds to a model and, with DataAnnotationsValidator, validates it against data-annotation attributes, surfacing validation messages in the UI.
Continue this course
- Previous: Desktop Apps with WPF & XAML
- Next: Nullable Reference Types — Compile-time null safety with #nullable, string? and flow analysis
- Quick reference: C# cheat sheet