A Simple To-Do List Manager Application in .NET Blazor | A Beginner Project

In this very quick tutorial, we will create a simple task(or to-do list) manager application 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

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.

Alejandrio Vasay
Alejandrio Vasay

Welcome to Coder Schmoder! I'm a .NET developer with a 15+ years of web and software development experience. I created this blog to impart my knowledge of programming to those who are interested in learning are just beginning in their programming journey.

I live in DFW, Texas and currently working as a .NET /Web Developer. I earned my Master of Computer Science degree from Texas A&M University, College Station, Texas. I hope, someday, to make enough money to travel to my birth country, the Philippines, and teach software development to people who don't have the means to learn it.

Articles: 24

Leave a Reply

Your email address will not be published. Required fields are marked *