Blazor学习之旅(9)用MudBlazor重构Todo
大家好,我是Edison。
在之前的学习之旅(3)开发一个Todo应用中,我们开发了一个简单版的Todo,这次我们基于MudBlazor来重构这个Todo应用。
Todo V1回顾
在Blazor入门学习(3)文章中,我们基于Blazor实现了一个简单版的Todo应用,它的效果如下:
(1)加载Todo列表
(2)添加新的Todo事项
可以看到,它仅仅实现了最基本的效果,但是如果涉及到分页、修改等操作,现有的界面就无法满足了。
因此,我们基于对MudBlazor组件库的了解,使用MudBlazor来重构一下这个Todo应用。
Todo V2规划
我们首先来做一个规划,期望效果是:
(1)能够有一个分页列表,能够将MongoDB中的数据读取出来并展示;
(2)能够针对Todo Name进行筛选查询;
(3)能够有一个弹出框进行新增Todo Item;
(4)能够有一个弹出框对选中的Todo Item进行修改;
(5)能够对选中的Todo Item进行删除,但要先给出确认删除的提示;
最后,新增、修改和删除操作成功都需要给出提示信息;
这里,我们以终为始,先来看看重构后的效果:
(1)分页列表展示
(2)根据Todo Item Name进行搜索
(3)新增TodoItem
(4)修改TodoItem
(5)删除TodoItem
Todo V2重构
(1)准备工作
在NavMenu.razor中新增一个导航菜单,名为TodoV2
<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> <div class="nav-item px-3"> <NavLink class="nav-link" href="todov2"> <span class="oi oi-list-rich" aria-hidden="true"></span>Todo v2 </NavLink> </div> </nav> </div>
(2)重构Todo列表页
在Pages目录下新增一个razor组件:TodoV2.razor,代码如下:
@page "/todov2" @using EDT.Todo.Application.Models.VO <PageTitle>Todo v2</PageTitle> <MudTable Items="@todos" Dense="true" Bordered="true" Striped="true" Hover="true" Breakpoint="Breakpoint.Sm" Filter="new Func<TodoItemVO, bool>(FilterTodoItems)" @bind-SelectedItem="selectedTodoItem"> <ToolBarContent> <MudText Typo="Typo.h6">My TodoItems (Total: @todos.Count(), NotFinished: @todos.Count(todo => !todo.IsComplete))</MudText> <MudSpacer /> <MudTextField @bind-Value="searchKeyword" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0" /> <MudSpacer /> <MudButton OnClick="@OpenCreateTodoDialog" Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Create" Color="Color.Primary"> Create New </MudButton> </ToolBarContent> <HeaderContent> <MudTh>Id</MudTh> <MudTh>Name</MudTh> <MudTh>Category</MudTh> <MudTh>IsComplete</MudTh> <MudTh>CheckItems</MudTh> <MudTh>Action</MudTh> </HeaderContent> <RowTemplate> <MudTd DataLabel="Id">@context.Id</MudTd> <MudTd DataLabel="Name">@context.Name</MudTd> <MudTd DataLabel="Category">@context.Category</MudTd> <MudTd DataLabel="IsComplete"> <MudSwitch @bind-Checked="@context.IsComplete" Color="Color.Primary" ReadOnly="true" /> </MudTd> <MudTd DataLabel="CheckItems">@String.Join(";", context.CheckItems)</MudTd> <MudTd DataLabel="Action"> <MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Update" Color="Color.Primary" OnClick="(async () => await OpenUpdateTodoDialog(context.Id))">Update</MudButton> <MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Delete" Color="Color.Secondary" OnClick="(async () => await DeleteTodoItem(context.Id))">Delete</MudButton> </MudTd> </RowTemplate> <PagerContent> <MudTablePager /> </PagerContent> </MudTable>
新增对应的C#代码类:TodoV2.razor.cs
using Microsoft.AspNetCore.Components; using MongoDB.Driver; using MudBlazor; using EDT.Todo.Application.Contracts.Business; using EDT.Todo.Application.Models.VO; using EDT.Todo.Portal.Shared; namespace EDT.Todo.Portal.Pages { public partial class TodoV2 { [Inject] public ISnackbar Snackbar { get; set; } [Inject] public IDialogService DialogService { get; set; } [Inject] public ITodoItemService TodoItemService { get; set; } [Inject] public ILogger<TodoV2> Logger { get; set; } private IList<TodoItemVO> todos = Array.Empty<TodoItemVO>(); private string searchKeyword = string.Empty; private TodoItemVO selectedTodoItem = null; protected override async Task OnInitializedAsync() { todos = await TodoItemService.GetTodoItems(ReadPreference.SecondaryPreferred); } private bool FilterTodoItems(TodoItemVO todoItem) => FilterTodoItemDetail(todoItem, searchKeyword); private bool FilterTodoItemDetail(TodoItemVO todoItem, string searchString) { if (string.IsNullOrWhiteSpace(searchString)) return true; if (todoItem.Name.Contains(searchString)) return true; if (todoItem.CheckItems.Contains(searchString)) return true; return false; } private async Task DeleteTodoItem(string id) { var parameters = new DialogParameters(); parameters.Add("ContentText", "Do you really want to delete this todoitem?"); parameters.Add("ButtonText", "Delete"); parameters.Add("Color", Color.Error); var options = new DialogOptions() { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; var dialog = DialogService.Show<ConfirmDialog>("Delete", parameters, options); var result = await dialog.Result; if (result.Cancelled || result.Data == null) return; try { // Delete TodoItem await TodoItemService.DeleteTodoItem(id); // Refresh Todos todos = await TodoItemService.GetTodoItems(ReadPreference.PrimaryPreferred); // Sucess Tip Snackbar.Add("Delete todoitem success!", Severity.Success); } catch (Exception ex) { Logger.LogError(ex, $"An exception happened during DeleteTodoItem {id}"); // Failed Tip Snackbar.Add("Delete todoitem failed!", Severity.Error); } } private async Task OpenCreateTodoDialog() { DialogOptions closeOnEscapeKey = new DialogOptions() { CloseOnEscapeKey = true }; var dialog = DialogService.Show<CreateTodoDialog>("Create Todo", closeOnEscapeKey); var result = await dialog.Result; if (result.Cancelled || result.Data == null) return; // Refresh Todos todos = await TodoItemService.GetTodoItems(ReadPreference.PrimaryPreferred); // Sucess Tip Snackbar.Add("Create todoitem success!", Severity.Success); } private async Task OpenUpdateTodoDialog(string todoItemId) { DialogOptions closeOnEscapeKey = new DialogOptions() { CloseOnEscapeKey = true }; var parameters = new DialogParameters { ["TodoItemId"] = todoItemId }; var dialog = DialogService.Show<UpdateTodoDialog>("Update Todo", parameters, closeOnEscapeKey); var result = await dialog.Result; if (result.Cancelled || result.Data == null) return; // Refresh Todos todos = await TodoItemService.GetTodoItems(ReadPreference.PrimaryPreferred); // Sucess Tip Snackbar.Add("Update todoitem success!", Severity.Success); } } }
在Todo列表页中,可以看到在Create和Update以及Delete时都进行了弹框操作,因此我们还需要实现几个Dialog。
(3)开发CreateTodoDialog
在CreateTodoDialog中,使用到了DialogContext 和 MudForm两个重要的标签,以很少的代码实现了一个原本需要用JS实现的对话框。
@using EDT.Todo.Domain.Enums <MudDialog> <DialogContent> <MudForm @ref="form" @bind-IsValid="@success"> <MudTextField T="string" Label="Name" @bind-Value="_todoItemDTO.Name" Required="true" RequiredError="Name is required!" Immediate="true"></MudTextField> <MudSelect T="Category" Label="Category" AnchorOrigin="Origin.BottomCenter" @bind-Value="_todoItemDTO.Category"> @foreach (var category in Enum.GetValues<Category>()) { <MudSelectItem T="Category" Value="category">@category.ToString()</MudSelectItem> } </MudSelect> <MudSwitch @bind-Checked="@_todoItemDTO.IsComplete" Color="Color.Primary" Label="Is Completed" /> </MudForm> </DialogContent> <DialogActions> <MudButton OnClick="Cancel">Cancel</MudButton> <MudButton Color="Color.Primary" Disabled="@(!success)" OnClick="@CreateTodoItem">Create</MudButton> </DialogActions> </MudDialog>
对应的C#代码类如下:CreateTodoDialog.razor.cs
public partial class CreateTodoDialog { [Inject] public ITodoItemService TodoItemService { get; set; } [CascadingParameter] MudDialogInstance MudDialog { get; set; } private MudForm form; private bool success = false; private TodoItemDTO _todoItemDTO = new(); private void Cancel() => MudDialog.Cancel(); private async Task CreateTodoItem() { var result = await TodoItemService.CreateTodoItem(_todoItemDTO); MudDialog.Close(result); } }
对于Dialog组件,默认需要一个级联参数MudDialogInstance,因此需要将其放在代码中。
此外,在此Dialog中还实现了调用Service类进行具体Create的操作。
(4)开发UpdateTodoDialog
开发完CreateTodoDialog后,UpdateTodoDialog就很简单了,复制过来改一下就OK。当然,你也可以将这两个操作放在同一个Dialog中进行。
@using EDT.Todo.Domain.Enums <MudDialog> <DialogContent> <MudForm @ref="form" @bind-IsValid="@success"> <MudTextField T="string" Label="Name" @bind-Value="_todoItemVO.Name" Required="true" RequiredError="Name is required!" Immediate="true"></MudTextField> <MudSelect T="Category" Label="Category" AnchorOrigin="Origin.BottomCenter" @bind-Value="_todoItemVO.Category"> @foreach (var category in Enum.GetValues<Category>()) { <MudSelectItem T="Category" Value="category">@category.ToString()</MudSelectItem> } </MudSelect> <MudSwitch @bind-Checked="@_todoItemVO.IsComplete" Color="Color.Primary" Label="Is Completed" /> </MudForm> </DialogContent> <DialogActions> <MudButton OnClick="Cancel">Cancel</MudButton> <MudButton Color="Color.Primary" Disabled="@(!success)" OnClick="@UpdateTodoItem">Update</MudButton> </DialogActions> </MudDialog>
对应的C#代码类如下:UpdateTodoDialog.razor.cs
public partial class UpdateTodoDialog { [Inject] public ITodoItemService TodoItemService { get; set; } [CascadingParameter] MudDialogInstance MudDialog { get; set; } [Parameter] public string TodoItemId { get; set; } private MudForm form; private bool success = false; private TodoItemVO _todoItemVO = new(); private void Cancel() => MudDialog.Cancel(); private async Task UpdateTodoItem() { var todoItemDTO = new TodoItemDTO { Id = TodoItemId, Name = _todoItemVO.Name, Category = _todoItemVO.Category, IsComplete = _todoItemVO.IsComplete }; await TodoItemService.UpdateTodoItem(todoItemDTO); MudDialog.Close(todoItemDTO); } protected override async Task OnInitializedAsync() { if (string.IsNullOrWhiteSpace(TodoItemId)) return; _todoItemVO = await TodoItemService.GetTodoItem(TodoItemId, ReadPreference.PrimaryPreferred); } }
对于Update操作,需要Todo列表页将需要修改的TodoItem的Id作为参数传递过来,并在初始化的时候调用Service进行数据的获取 和 绑定。
(5)开发通用ConfirmDialog
对于ConfirmDialog而言,它本身并没有任何逻辑,而且可以被任意页面进行复用,只是提示的消息内容不同而已。
因此,我们在Shared目录下创建一个ConfirmDialog.razor:
<MudDialog> <DialogContent> <MudText>@ContentText</MudText> </DialogContent> <DialogActions> <MudButton OnClick="Cancel">Cancel</MudButton> <MudButton Color="@Color" Variant="Variant.Filled" OnClick="Submit">@ButtonText</MudButton> </DialogActions> </MudDialog> @code { [CascadingParameter] MudDialogInstance MudDialog { get; set; } [Parameter] public string ContentText { get; set; } [Parameter] public string ButtonText { get; set; } [Parameter] public Color Color { get; set; } void Submit() => MudDialog.Close(DialogResult.Ok(true)); void Cancel() => MudDialog.Cancel(); }
由于该页面代码很简单,我们就直接将其放在同一个razor中,不区分前后端的部分类。
小结
本篇,我们试着将之前的Todo应用使用MudBlazor来重构一下,相比之前会有一些互动了,但也仅仅是展示了最基本的界面。实际上,我们可以基于MudBlazor开发更加好看一点的界面和互动效果,这就等待你自己去探索了。
下一篇,我们学习在Blazor如何实现本地化及多语言支持。
参考资料
MudBlazor官网Doc文档