Blazor学习之旅(3)实现一个Todo应用

大家好,我是Edison。

最近在学习Blazor做全栈开发,因此根据老习惯,我会将我的学习过程记录下来,一来体系化整理,二来作为笔记供将来翻看。

本篇,我们通过一个简单的Todo示例应用来介绍如何实现基础的数据绑定和事件。
添加Todo组件

在Pages目录下,新增一个Razor组件,命名:Todo.razor.

@page "/todo"
<h3>Todo</h3>
@code {
}

将Todo组件添加到导航栏

我们知道,在Shared目录下的NavMenu组件用于应用的导航,因此我们需要将Todo组件加进去以便可以访问到:

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">    <nav class="flex-column">
        ...
        <div class="nav-item px-3">            <NavLink class="nav-link" href="todo">                <span class="oi oi-list-rich" aria-hidden="true"></span> Todo            </NavLink>        </div>    </nav></div>

这时导航栏中也就有Todo了:

Blazor学习之旅(3)实现一个Todo应用

添加Model

添加一个Models目录,在此目录下新建一个TodoItem类:

namespace EDT.BlazorServer.App.Models{    public class TodoItem    {        public string Id { get; set; }        public string? Name { get; set; }        public bool IsComplete { get; set; }        public string? Remark { get; set; }    }}

为了模拟实现数据库访问的效果,这里我们使用EF Core的内存数据库来模拟。

首先,添加对Microsoft.EntityFrameworkCore.InMemory的应用。

其次,在Models目录下创建一个TodoContext类:

using Microsoft.EntityFrameworkCore;
namespace EDT.BlazorServer.App.Models{    public class TodoContext : DbContext    {        public TodoContext(DbContextOptions<TodoContext> options)            : base(options)        {        }
        public DbSet<TodoItem> TodoItems { get; set; }    }}

然后,在Program.cs中注入这个DbContext:

// Add database contextbuilder.Services.AddDbContext<TodoContext>(opt =>    opt.UseInMemoryDatabase("TodoList"));

添加种子数据

为了方便演示,我们提前准备一些SeedData,创建一个SeedData的静态类:

namespace EDT.BlazorServer.App.Models{    public static class SeedData    {        public static void Initialize(TodoContext db)        {            var todos = new TodoItem[]            {                new TodoItem { Id = Guid.NewGuid().ToString(), Name = "Study Computer Network", IsComplete=false, Remark = "Take a Test" },                new TodoItem { Id = Guid.NewGuid().ToString(), Name = "Study Operation System", IsComplete=false, Remark = "Take a Test" },                new TodoItem { Id = Guid.NewGuid().ToString(), Name = "Study Data Structure", IsComplete=false, Remark = "Take a Test" },                new TodoItem { Id = Guid.NewGuid().ToString(), Name = "Walk the dog", IsComplete=true, Remark = string.Empty },                new TodoItem { Id = Guid.NewGuid().ToString(), Name = "Run 5km in 40mins", IsComplete=true, Remark = string.Empty },            };
            db.TodoItems.AddRange(todos);            db.SaveChanges();        }    }}

然后,在Program.cs中确保运行这个初始化操作:

添加Service

假设我们所有的TodoItem都是通过Service来完成的,不直接在Pages下的组件中来操作。

首先,创建一个接口ITodoItemService:

using EDT.BlazorServer.App.Models;
namespace EDT.BlazorServer.App.Service.Contracts{    public interface ITodoItemService    {        Task<IList<TodoItem>> GetTodoItemsAsync();        Task<TodoItem> GetTodoItemAsync(string id);        Task<TodoItem> AddTodoItemAsync(TodoItem todoItem);        Task<TodoItem> UpdateTodoItemAsync(TodoItem todoItem);        Task<TodoItem> DeleteTodoItemAsync(TodoItem todoItem);    }}

这时,我们重新启动应用就可以看到Counter组件显示在主页上面了:

Blazor学习之旅(3)实现一个Todo应用

其次,实现TodoItemService:

using EDT.BlazorServer.App.Models;using EDT.BlazorServer.App.Service.Contracts;using Microsoft.EntityFrameworkCore;
namespace EDT.BlazorServer.App.Service{    public class TodoItemService : ITodoItemService    {        private readonly TodoContext _todoContext;
        public TodoItemService(TodoContext todoContext)        {            _todoContext = todoContext;        }        
        public async Task<TodoItem> AddTodoItemAsync(TodoItem todoItem)        {            todoItem.Id = Guid.NewGuid().ToString();            todoItem.IsComplete = false;
            _todoContext.TodoItems.Add(todoItem);            await _todoContext.SaveChangesAsync();
            return todoItem;        }        
        public async Task<TodoItem> DeleteTodoItemAsync(TodoItem todoItem)        {            _todoContext.TodoItems.Remove(todoItem);            await _todoContext.SaveChangesAsync();
            return todoItem;        }        
        public async Task<TodoItem> GetTodoItemAsync(string id)        {            return await _todoContext.TodoItems.FirstOrDefaultAsync(t => t.Id == id);        }        
        public async Task<IList<TodoItem>> GetTodoItemsAsync()        {            return await _todoContext.TodoItems.ToListAsync();        }                 
        public async Task<TodoItem> UpdateTodoItemAsync(TodoItem todoItem)        {            _todoContext.TodoItems.Update(todoItem);            await _todoContext.SaveChangesAsync();
            return todoItem;        }    }}

完善Todo组件

这里,我们仿照FetchData组件添加一个表格 并 实现TodoItem的添加:

@page "/todo"@using EDT.BlazorServer.App.Models@using EDT.BlazorServer.App.Service.Contracts@inject ITodoItemService todoItemService;
<h3>Todo (@todos.Count(todo => !todo.IsComplete))</h3>
@if (todos == null){    <p><em>Loading...</em></p>}else{    <table class="table">        <thead>            <tr>                <th>Id</th>                <th>Name</th>                <th>IsComplete</th>                <th>Remark</th>            </tr>        </thead>        <tbody>            @foreach (var todo in todos)            {                <tr>                    <td>@todo.Id.ToString()</td>                    <td>@todo.Name</td>                    <td><input type="checkbox" @bind="todo.IsComplete" /></td>                    <td>@todo.Remark</td>                </tr>            }        </tbody>    </table>}
<input placeholder="Todo Item Name (Necessary)" @bind="newTodoItemName" /><input placeholder="Todo Item Remark (Optioinal)" @bind="newTodoItemRemark" /><button @onclick="AddTodo">Add todo</button>
@code {    private IList<TodoItem> todos;    private string? newTodoItemName;    private string? newTodoItemRemark;
    protected override async Task OnInitializedAsync()    {        todos = await todoItemService.GetTodoItemsAsync();    }
    private async void AddTodo()    {        if (string.IsNullOrWhiteSpace(newTodoItemName))            return;
        var todoItem = new TodoItem { Name = newTodoItemName, Remark = newTodoItemRemark };        await todoItemService.AddTodoItemAsync(todoItem);        // Clear Textboxes        newTodoItemName = newTodoItemRemark = string.Empty;        // Refresh Todos        todos = await todoItemService.GetTodoItemsAsync();  }}

需要注意的是:

(1)通过@injec指令进行Service的注入,和常见的构造函数注入不同。

(2)通过重写OnInitializeAsync事件,进行数据的初始化,即从数据库中读取TodoItem的列表。这部分属于Blazor组件的生命周期范畴,这里不过多纠结即可。唯一需要了解的是,OnInitialized 和 OnInitializeAsync 事件是在做组件的初始化,它发生在参数注入完成之后(这里的ITodoItemService就是注入的参数)。

(3)除了foreach,Blazor还包含其他循环指令,例如 @for、@while 和 @do while。这些指令返回重复的标记块。它们的工作方式与等效的 C# for、while 和 do...while 循环类似。

到此,最终的项目结构如下图所示:

Blazor学习之旅(3)实现一个Todo应用

运行效果

运行起来的效果如下图所示:

(1)加载Todo列表

Blazor学习之旅(3)实现一个Todo应用

(2)添加新的Todo事项

Blazor学习之旅(3)实现一个Todo应用

小结

本篇,我们实现了一个Todo应用。

下一篇,我们学习一下在Blazor中数据是如何被共享的。

参考资料

Microsoft Docs,《使用Blazor生成Web应用》