Part 3 built a read-only Task Tracker page with Razor, HTML generated from data, nothing more, deliberately. This post takes that exact same Razor syntax and adds real interactivity on top of it: adding a task, checking a box, deleting one, filtering by status, all using C#, with no JavaScript written anywhere, and no full page reload on any interaction.
What Blazor Actually Is
Blazor is a framework for building interactive web UIs using C# instead of JavaScript. It's built directly on top of Razor syntax, covered in the previous post, the same @ symbol, the same mixing of C# and HTML, with an added layer that makes the page actually respond to user interaction.
Think of Part 3's Razor page as a printed photograph, accurate, but fixed the moment it was generated. Blazor turns that into a live video call instead, the picture keeps updating in real time as things actually change, without needing to print a brand new photograph, reload the whole page, every time something happens.
Components: The Actual Building Block
A Blazor component is a .razor file that combines markup and its C# logic together in one place, genuinely different from WPF's split between a .xaml file and a separate .xaml.cs code-behind file.
@* TaskTracker.razor - markup AND logic, ONE file *@
<h2>Task Tracker</h2>
<p>@completedCount of @tasks.Count tasks completed</p>
@code {
private List<TaskItem> tasks = new();
private int completedCount => tasks.Count(t => t.IsCompleted);
}
<!-- The @code block contains genuine C# - fields,
properties, methods - living directly alongside
the markup that uses them, in the exact same file -->
Event Handling: @onclick, Wiring a Button to Real CSharp
Blazor provides directives like @onclick that wire a DOM event directly to a C# method, no JavaScript event listener setup required at all.
<input @bind="newTaskDescription" placeholder="New task..." />
<button @onclick="AddTask">Add Task</button>
@code {
private string newTaskDescription = string.Empty;
private List<TaskItem> tasks = new();
private void AddTask()
{
if (!string.IsNullOrWhiteSpace(newTaskDescription))
{
tasks.Add(new TaskItem { Description = newTaskDescription, IsCompleted = false });
newTaskDescription = string.Empty;
}
}
}
<!-- Clicking the button calls AddTask() directly.
Blazor automatically re-renders whatever part of
the page actually changed as a RESULT of that
method running - no manual "refresh the UI" step,
similar in spirit to what INotifyPropertyChanged
did for WPF in the MVVM post, but handled
automatically by the framework here instead -->
@bind: Two-Way Data Binding
@bind keeps an input element and a C# variable automatically in sync, in both directions, type into the box, the variable updates; change the variable in code, the box reflects it.
<input @bind="newTaskDescription" />
<!-- Typing into this input automatically updates
newTaskDescription. No manual @onchange handler
needed to read the typed value - @bind does
both directions of the sync automatically -->
<input type="checkbox" @bind="task.IsCompleted" />
<!-- The exact same @bind directive, on a checkbox
this time - checking or unchecking it updates
task.IsCompleted directly, immediately -->
Blazor Server vs Blazor WebAssembly: A Genuinely Important Distinction
Blazor has two fundamentally different hosting models, and the C# code you write looks nearly identical in both, but where that code actually executes is completely different.
With Blazor Server, your C# code runs on the server. The browser holds a live connection, via SignalR, back to that server, every click, every keystroke, sends a small message to the server, which runs the actual C# and sends back only the specific UI changes needed. The advantage is a smaller initial download and full .NET access. The tradeoff is that it requires a constant, low-latency connection to the server, it doesn't work offline, and every interaction has a small network round-trip.
With Blazor WebAssembly, your C# code is compiled and runs directly inside the browser itself, via WebAssembly, no server round-trip needed for each interaction. The advantage is that it works offline once loaded, with no per-interaction network latency. The tradeoff is a larger initial download, since the .NET runtime itself has to be downloaded to the browser, and some API limitations compared to full server-side .NET.
Think of Blazor Server like a video game streamed from a powerful remote server, the actual game runs elsewhere, your screen just shows the result and sends your button presses back. Blazor WebAssembly is like downloading the entire game and running it natively on your own machine, a bigger download upfront, but no lag once it's running, and no dependency on staying connected to anything.
The Complete Interactive Task Tracker
@page "/tasks"
<h2>Task Tracker</h2>
<div>
<input @bind="newTaskDescription" placeholder="New task..." />
<button @onclick="AddTask">Add Task</button>
</div>
<ul>
@foreach (var task in FilteredTasks)
{
<li>
<input type="checkbox" @bind="task.IsCompleted" />
@if (task.IsCompleted)
{
<s>@task.Description</s>
}
else
{
<span>@task.Description</span>
}
<button @onclick="() => DeleteTask(task)">Delete</button>
</li>
}
</ul>
<div>
<button @onclick='() => currentFilter = "All"'>All</button>
<button @onclick='() => currentFilter = "Active"'>Active</button>
<button @onclick='() => currentFilter = "Completed"'>Completed</button>
</div>
@code {
private List<TaskItem> tasks = new();
private string newTaskDescription = string.Empty;
private string currentFilter = "All";
private IEnumerable<TaskItem> FilteredTasks => currentFilter switch
{
"Active" => tasks.Where(t => !t.IsCompleted),
"Completed" => tasks.Where(t => t.IsCompleted),
_ => tasks
};
private void AddTask()
{
if (!string.IsNullOrWhiteSpace(newTaskDescription))
{
tasks.Add(new TaskItem { Description = newTaskDescription, IsCompleted = false });
newTaskDescription = string.Empty;
}
}
private void DeleteTask(TaskItem task) => tasks.Remove(task);
}
// TaskItem.cs - a plain class, same shape as every
// previous post in this series
public class TaskItem
{
public string Description { get; set; }
public bool IsCompleted { get; set; }
}
Worth noticing directly: FilteredTasks is a computed property, not a method that has to be manually called after every change, Blazor automatically re-evaluates it and re-renders the list whenever any value it depends on changes, whether that's currentFilter being set by a button click, or a task's IsCompleted flipping via a checkbox. There is no manual refresh step anywhere in this file, the same category of problem the MVVM post solved for WPF, here solved automatically by the framework itself.
Comparing This to the WPF/MVVM Version
In WPF plus MVVM from Parts 1 and 2, you had a Window or UserControl; in Blazor, that's a Component, a single .razor file. WPF needed XAML plus a separate code-behind file; Blazor combines markup and @code in one file. WPF used Click="Method"; Blazor uses @onclick="Method". WPF used {Binding Property}; Blazor uses @bind="variable". WPF needed INotifyPropertyChanged with manual event wiring; Blazor re-renders automatically, handled by the framework. WPF used ICommand and RelayCommand; Blazor just calls a plain C# method directly.
The underlying goal is genuinely the same across both, keep the UI in sync with changing data, without manual refresh steps, but Blazor folds markup and logic into a single file and handles re-rendering automatically, where WPF and MVVM required more explicit, hand-written plumbing to achieve a similar result.
Key Lessons
A Blazor component combines markup and C# logic in one .razor file, genuinely different from WPF's split between XAML and a separate code-behind file.
@onclick wires a DOM event directly to a C# method, with no JavaScript event listener setup required.
@bind provides automatic two-way synchronization between an input element and a C# variable, type in the box, the variable updates; change the variable, the box updates.
Blazor Server runs C# on the server with a live connection back to the browser; Blazor WebAssembly compiles C# to run directly inside the browser itself, genuinely different tradeoffs in latency, offline support, and initial download size.
Blazor re-renders automatically when a value a component depends on changes, no manual refresh call anywhere, solving the same category of problem MVVM solved for WPF, but handled by the framework rather than hand-written.
What's Next
Part 5, the final post in this series, applies Bootstrap directly to this Blazor Task Tracker, the grid system, styled buttons, cards, and a proper navbar, turning this functional-but-plain interface into something that actually looks presentable.
Summary
Blazor takes the Razor syntax from the previous post and adds genuine interactivity on top of it, using C# instead of JavaScript throughout. Components combine markup and logic in one file. @onclick and @bind replace manual event listener setup with simple, declarative directives. And critically, Blazor handles re-rendering automatically whenever a dependent value changes, the same underlying problem MVVM solved for WPF through INotifyPropertyChanged, here solved by the framework itself rather than requiring hand-written plumbing. The choice between Blazor Server and Blazor WebAssembly is a genuine architectural decision, not a minor detail, since it determines where the actual C# code runs and what tradeoffs that brings.
More from TechStack Blog: C# / .NET: https://www.techstackblog.com/category.html?cat=csharp
CS Fundamentals: https://www.techstackblog.com/category.html?cat=cs-fundamentals
Top comments (0)