In this very quick tutorial, we will create a simple to-do list app (or task manager) using .NET Blazor using JavaScript interop for persistent local storage, acting as a database. This tutorial is aimed at beginner Blazor developers and will guide you through building a to-do list application where users can add, edit, and delete tasks, and the data will persist across sessions using the browser’s local storage. The application that I created in this demo was Blazor Progressive Web Application (PWA), but Blazor WASM application should work also.
Summary
In this quick tutorial, you’ll learn key Blazor concepts such as:
- How to create a Blazor form with EditForm, and its built-in input components like InputText and InputCheckbox.
- How to implement simple Create, Read, and Delete task functionalities.
- How to “cross out” a completed task using css.
- How to interact with JavaScript Interop, allowing you to perform browser-related tasks like saving and retrieving data using the browser’s local storage.
- How to implement Event handling using
ValueChanged
for handling the event when the checkbox is checked, as opposed to using @onchange directive to trigger the event handler.
By the end of this tutorial, you’ll have a good grasp of these core Blazor concepts and will be able to expand on them to create more complex applications.
Content
- Create the UI with EditForm, InputText, and InputCheckBox.
- Implement Add, Read, and Delete functionalities in C#
- Accessing browser’s local storage via JS Interop
- Handling Checkbox Event With ValueChanged
The Application in Action
Requirement:
- App prompts user for a task in an input box. User can either hit Enter button in their keyboard, or click on The “Add Task” button.
- Tasks are stored as List, and are displayed as a list of tasks – a task is displayed with checkbox in front of it, and a “Delete” button after it as the image above shows.
- To indicate task is completed, user checks the checkbox – creating a strikethrough effect on the text.
- App shall use a JavaScript interop for the purpose of using a persistent local storage in which we will store the tasks.
- Delete button, of course, deletes a task.
Create the Project
Create a new Blazor project using Visual Studio or the command line.
dotnet new blazorwasm -o TaskManagerApp --pwa
cd TaskManagerApp
dotnet run
Create the Model
We’ll need a class to represent a task. Add a ToDoItem
class in the Models
folder.
// Models/ToDoItem.cs
public class ToDoItem
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
}
Create the UI with EditForm, InputText, and InputCheckBox
In the Pages
folder, modify the Home.razor
file to include the to-do list functionality using Blazor forms. Below is the UI part of our Blazor component using EditForm and input components to allow the user to enter a new task, delete a task, and “cross out” a task.
@page "/"
@using System.Text.Json
@inject IJSRuntime JSRuntime
<h3>To-Do List</h3>
<div class="form-container">
<EditForm Model="@newTask" OnValidSubmit="AddTask">
<div class="mb-3">
<label for="taskTitle">Task Title</label>
<InputText id="taskTitle" class="form-control" @bind-Value="newTask.Title" />
</div>
<button class="btn btn-primary" type="submit">Add Task</button>
</EditForm>
<div class="tasks-container">
<h4>Your Tasks</h4>
<ul class="list-group">
@foreach (var task in tasks)
{
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<InputCheckbox
Value="task.IsCompleted"
ValueChanged="@(args => OnTaskCompletionChanged(task))"
ValueExpression="@( () => task.IsCompleted )" />
<span class="@((task.IsCompleted ? "text-decoration-line-through" : ""))">@task.Title</span>
</div>
<button class="btn btn-danger btn-sm" @onclick="() => RemoveTask(task)">Delete</button>
</li>
}
</ul>
@if(msg != null)
{
<h2>@msg</h2>
}
</div>
</div>
Implement Create, Read and Delete Task Functionalities
This is a simple application that adds, deletes and loads the tasks to and from the form. We won’t need to implement any update functionality.
@code {
private List<ToDoItem> tasks = new List<ToDoItem>();
private ToDoItem newTask = new ToDoItem();
private string msg;
protected override async Task OnInitializedAsync()
{
await LoadTasksFromLocalStorage();
}
private async Task AddTask()
{
if (!string.IsNullOrWhiteSpace(newTask.Title))
{
tasks.Add(new ToDoItem { Title = newTask.Title, IsCompleted = false });
newTask = new ToDoItem();
await SaveTasksToLocalStorage();
}
}
private async Task RemoveTask(ToDoItem task)
{
tasks.Remove(task);
await SaveTasksToLocalStorage();
}
// Trigger save when the checkbox is toggled
private async Task OnTaskCompletionChanged(ToDoItem task)
{
// Task completion state is already updated, we just save the tasks list
task.IsCompleted = true;
await SaveTasksToLocalStorage();
}
private async Task LoadTasksFromLocalStorage()
{
var tasksJson = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "tasks");
if (!string.IsNullOrEmpty(tasksJson))
{
tasks = JsonSerializer.Deserialize<List<ToDoItem>>(tasksJson) ?? new List<ToDoItem>();
}
}
private async Task SaveTasksToLocalStorage()
{
var tasksJson = JsonSerializer.Serialize(tasks);
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "tasks", tasksJson);
}
}
In addition to the create, read and delete methods, we also implemented an event handler on line #30. This is called when the user check the checkbox.
Access Browser’s Local Storage via JS Interop
JSRuntime
is used to store and retrieve tasks from the browser’s local storage so the tasks persist across browser sessions. I discuss this further below. Blazor provides JavaScript interop for calling JavaScript functions via @inject IJSRuntime JSRuntime on line #4. Then on line #90, we’re using localStorage
to persist the tasks. This doesn’t require any additional JavaScript files because we’re using the built-in localStorage
object.
We access the browser’s local storage via .NET’s JSRuntime Class.
Inject Service
To be able to use JSRuntime, we need to inject the JSRuntime service into our Blazor component, like so:
@inject IJSRuntime JSRuntime
as shown on line #4 of Home.razor source code above.
Add item to local storage
To add to the local storage, we do the following(line #50 of our C# code above):
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "tasks", tasksJson);
The argument localStorage.setItem
tells the service to add tasksJson
object into the cache identified uniquely by the string “tasks”.
Read item from local storage
To read from the local storage, we do the following(line #40 of our C# code above):
var tasksJson = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "tasks");
The argument localStorage.getItem
tells the service to get a tasksJson
object from the cache identified uniquely by the string “tasks”.
Handling Checkbox Event
Checkbox event handler. When the user checks the checkbox, the OnTaskCompletionChanged()
event handler is called. As you notice in the code above, I use the combination of Value
(value of the input), ValueChanged
(event handler), and ValueExpression
(binding to model field).
Note: To trigger the “checked” event, we cannot use both @bind-Value
and @onchange
directives together. Neither can you use @bind-Value
and ValueChanged
event handler. Doing so will suppress @onchange
or ValueChanged
, and your event hander will not be called.
I will discuss event handling in greater details in my future articles.
Conclusion
Congratulations! You’ve successfully created a fully functional task manager application using Blazor. Along the way, you learned how to use Custom event handling with ValueChanged
, which offers finer control over updating the state of your application, allowing you to trigger specific behavior before committing changes to your data model. You also learned how to use JavaScript interop, an important feature of Blazor that lets you bridge the gap between C# and JavaScript, making it possible to interact with browser-specific features like local storage.