《进击吧!Blazor!》第一章 3.页面制作
作者介绍
陈超超
Ant Design Blazor 项目贡献者
拥有十多年从业经验,长期基于.Net技术栈进行架构与开发产品的工作,Ant Design Blazor 项目贡献者,现就职于正泰集团
写专栏开头老规矩了,所以……先来段广告😁
《进击吧!Blazor!》是本人与张善友老师合作的Blazor零基础入门系列视频,此系列能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力。
视频地址:https://space.bilibili.com/483888821/channel/detail?cid=151273
演示代码:https://github.com/TimChen44/Blazor-ToDo
本系列文章是基于《进击吧!Blazor!》直播内容编写,升级.Net5,改进问题,讲解更全面。
从这次分享开始我通过制作一个ToDo应用来介绍Balzor的开发。
准备工作
项目准备
- 打开上一次分享内容创建项目
2.修改\wwwroot\css\app.css
文件,只保留以下代码用于配置程序发生未捕获异常时的提示样式
#blazor-error-ui { background: lightyellow; bottom: 0; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed; width: 100%; z-index: 1000; } #blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; }
- 修改
index.htm
文件,移除对‘bootstrap’样式的引用,因为我们使用ant-design-blazor
来做UI
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /><!--此行代码删除-->
引入ant-design-blazor包
✨ 特性
🌈 提炼自企业级中后台产品的交互语言和视觉风格。
📦 开箱即用的高质量 Razor 组件,可在多种托管方式共享。
💕 支持基于 WebAssembly 的客户端和基于 SignalR 的服务端 UI 事件交互。
🎨 支持渐进式 Web 应用(PWA)
🛡 使用 C# 构建,多范式静态语言带来高效的开发体验。
⚙️ 基于 .NET Standard 2.1/.NET 5,可直接引用丰富的 .NET 类库。
🎁 可与已有的 ASP.NET Core MVC、Razor Pages 项目无缝集成。
项目地址:https://github.com/ant-design-blazor/ant-design-blazor
文档地址:https://antblazor.com/
安装
- 用NuGet安装AntDesign包
Install-Package AntDesign -Version 0.5.3
- 在
Program.cs
中注册:
public static async Task Main(string[] args) { //其他代码 builder.Services.AddAntDesign(); await builder.Build().RunAsync(); }
- 在
wwwroot/index.html
中引入静态文件:
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet"> <script src="_content/AntDesign/js/ant-design-blazor.js"></script>
- 在
_Imports.razor
中加入命名空间
@using AntDesign
- 为了动态地显示弹出组件,需要在
App.razor
末尾添加一个<AntContainer />
组件。
<AntContainer /> <!--添加在这里-->
路由
在页面中切换,必定使用路由,我们先了解一下blazor
的路由机制App.razor
文件
<Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router>
在上面第一行把当前项目的程序集赋值给了 Router
组件的 AppAssembly
属性,这样程序在启动时检索程序集中所有的页面用于路由,路由信息通过页面文件顶部的 @page
标记进行定义。还可以通过 AdditionalAssemblies
属性支持多个程序集。
Route里面有两个模板属性,分别是路由命中和未命中显示的内容、RouteView
组件用于显示路由的页面,这里从 Router
接收 routeData
以及任何所需的参数。DefaultLayout="@typeof(MainLayout)"
定义了默认布局。
布局文件及菜单
编辑 Shared/MainLayout.razor
文件,制作程序的布局以及菜单。
@inherits LayoutComponentBase <Layout> <Sider Style="overflow: auto;height: 100vh;position: fixed;left: 0;"> <div class="logo"> 进击吧!Blazor! </div> <Menu Theme="MenuTheme.Dark"> <MenuItem RouterLink="/"> 主页 </MenuItem> <MenuItem RouterLink="/today" RouterMatch="NavLinkMatch.Prefix"> 我的一天 </MenuItem> <MenuItem RouterLink="/search" RouterMatch="NavLinkMatch.Prefix"> 全部 </MenuItem> </Menu> </Sider> <Layout Class="site-layout"> @Body </Layout> </Layout> <style> <!--为了减少文档代码量,此处省略样式代码,大家可以直接从本项目源码查看,后面的示例代码采用相同模式,将不再赘述--> </style>
Layout
页面布局组件
Layout组件帮助文档:https://antblazor.com/zh-CN/components/layout
Menu 菜单组件
Theme="MenuTheme.Dark"黑色主题
Menu组件帮助文档:https://antblazor.com/zh-CN/components/menu
MenuItem
菜单项组件RouterLink="/"
路由地址RouterMatch="NavLinkMatch.Prefix"
路由匹配模式,通过匹配 URL 来切换 active CSS 类,这有助于在导航菜单中显示那个页面是活动页。NavLinkMatch.All
:NavLink 在与当前整个 URL 匹配的情况下处于活动状态。NavLinkMatch.Prefix(默认)
:NavLink 在与当前 URL 的任何前缀匹配的情况下处于活动状态。
@Body
通过这个固定语法在布局中标记指定呈现内容的位置。
主页
编辑 Pages/Index.razor
文件
@page "/" <Result Icon="smile-outline" Title="@("进击吧!Blazor!")"></Result>"
这个主页左边是菜单,右边是内容,符合上一节布局格式,因为主页路由地址是/,所以默认就打开了。
@page "/"
页面路由地址
Result
结果组件,用于反馈一系列操作任务的处理结果,主页虽然是不反馈结果,不过当成ToDo应用门面效果还不错😃
Result组件帮助文档:https://antblazor.com/zh-CN/components/result
我的一天
一个用于显示和维护当天待办事项的界面
创建Pages/ToDay.razor
文件
@page "/today" <PageHeader Title="@("我的一天")" Subtitle="@DateTime.Now.ToString("yyyy年MM月dd日")"></PageHeader>
启动后点击左边的“我的一天”菜单就可以导航到刚刚创建的页面,目前就只有一个页头。
@page "/today"
设置当前页路由地址为/today
PageHeader
页头信息
PageHeader组件帮助文档:https://antblazor.com/zh-CN/components/pageheader
待办列表
ToDo的灵魂那就是待办列表了,那么三步走:先上代码,再看效果,最后讲解😉
@inject TaskServices TaskSvr @foreach (var item in taskDtos) { <Card Bordered="true" Size="small" Class="task-card"> <div class="task-card-item"> <div class="title"> <Text Strong> @item.Title</Text> <br /> <Text Type="@TextElementType.Secondary">@item.Description</Text> </div> </div> </Card> } @code{ private List<TaskDto> taskDtos = new List<TaskDto>(); protected async override Task OnInitializedAsync() { taskDtos = await TaskSvr.LoadToDay(); await base.OnInitializedAsync(); } }
效果图
通过OnInitializedAsync
方法中使用TaskSvr.LoadToDay()
载入待办数据后存入taskDtos
变量,最后通过@foreach
遍历taskDtos
集合,以Card
组件作为容器,使用@item.Title
和@item.Description
将数据单项绑定到界面显示。
@foreach (var item in taskDtos) { }
这个和C#
中的foreach
功能相同@
标记可以把变量值单向绑定到页面中@code{}
在razor
语法中用于标记{}
中可以插入c#
代码
@inject TaskServices TaskSvr
通过依赖注入TaskServices
服务
关于依赖注入会在下一章节专题介绍,此处就不展开了
Card
卡片容器Bordered="true"
显示卡片边框Size="small"
小尺寸卡片
Card组件帮助文档:https://antblazor.com/zh-CN/components/card
标记重要
有些待办肯定比其他待办更重要,所以增加一个标记重要的按钮,老规矩:先上代码,再看效果,最后讲解😉
<Text Type="@TextElementType.Secondary">@item.Description</Text> </div> <!--从这里开始插入以下代码--> <div class="star" @onclick="x => OnStar(item)"> <Icon Type="star" Theme="@(item.IsImportant ? "fill" : "outline")" /> </div>
private void OnStar(TaskDto task) { task.IsImportant = !task.IsImportant; }
用div
包裹一个Icon
组件,然后在div
上注册@onclick
点击事件,当点击后会触发private void OnStar(TaskDto task)
方法,并将当前项目item
作为参数传入,方法中修改了TaskDto
的IsImportant
属性值,通过@(item.IsImportant ? "fill" : "outline")
单向绑定,实现修改Icon
组件的Theme
样式在fill
和outline
切换。
@()
相比@
标记,它可以在()
括号中使用单行代码进行单向绑定。@onclick
事件绑定,除了onclick
还有很多,详见ASP.NET Core Blazor事件处理
Icon
语义化的矢量图形。Type="star"
图标名称
Icon组件帮助文档:https://antblazor.com/zh-CN/components/icon
计划时间
既然是待办,那么必然有一个计划开始时间PlanTime
,以及一个截至时间Deadline
,所以老规矩,三步走:先上代码,再看效果,最后讲解😉
<Text Type="@TextElementType.Secondary">@item.Description</Text> </div> <!--从这里开始插入以下代码--> <div class="date"> @item.PlanTime.ToShortDateString() <br /> @{ int? days = (int?)item.Deadline?.Subtract(DateTime.Now.Date).TotalDays; } <span style="color:@(days switch { _ when days > 3 => "#ccc", _ when days > 0 => "#ffd800", _ => "#ff0000" })"> @item.Deadline?.ToShortDateString() </span> </div>
上面显示计划日期PlanTime
,下面显示Deadline
,并通过与当前时间对比,根据时间差决定显示方式。
days switch { _ when days > 3 => "#ccc", _ when days > 0 => "#ffd800", _ => "#ff0000" }
这是switch
表达式写法,可以简化代码,如果使用if
代码将比较臃肿,代码如下
@if (days > 3) { <span style="color:#ccc"> @item.Deadline?.ToShortDateString() </span> } else if (days > 0) { <span style="color:#ff6a00"> @item.Deadline?.ToShortDateString() </span> } else { <span style="color:#ff0000"> @item.Deadline?.ToShortDateString() </span> }
待办详情
列表只适合查看待办概要,需要查看详情还需独立页面,所以我们做一个抽屉详情页,那么我们三步走
编辑ToDay.razor
文件
<div class="title" @onclick="x=>OnCardClick(item)"> <Text Strong> @item.Title</Text> <br /> <Text Type="@TextElementType.Secondary">@item.Description</Text> </div>
[Inject] public DrawerService DrawerSrv { get; set; } async void OnCardClick(TaskDto task) { var options = new DrawerOptions() { Title = task.Title, Width = 450, }; await DrawerSrv.CreateDialogAsync<TaskInfo, TaskDto, TaskDto>(options, task); await InvokeAsync(StateHasChanged); }
新建TaskInfo.razor
文件
@inherits DrawerTemplate<TaskDto, TaskDto> <Form Model="this.Options" LabelCol="new ColLayoutParam() {Span = 8 }"> <FormItem Label="标题"> <Input @bind-Value="context.Title" /> </FormItem> <FormItem Label="计划日期"> <DatePicker @bind-Value="context.PlanTime" Picker="@DatePickerType.Date" /> </FormItem> <FormItem Label="截至日期"> <DatePicker @bind-Value="context.Deadline" Picker="@DatePickerType.Date" /> </FormItem> <FormItem Label="描述"> <TextArea @bind-Value="context.Description" MinRows="4" /> </FormItem> <FormItem Label="重要"> <Switch @bind-Value="context.IsImportant" /> </FormItem> <FormItem Label="完成"> <Switch @bind-Value="context.IsFinish" /> </FormItem> </Form>
在之前的<div class="title">
中添加@onclick="x=>OnCardClick(item)"
注册点击事件触发async void OnCardClick(TaskDto task)
方法,然后使用DrawerSrv.CreateDialogAsync
方法打开一个抽屉,抽屉中包含TaskInfo
组件,当抽屉关闭时用InvokeAsync
更新页面。
async/await
异步等待,可以让异步操作的代码变成同步编码风格,此处CreateDialogAsync
是一个异步过程,通过它让他进行异步等待,只有在抽屉关闭后才会继续执行后面的await InvokeAsync(StateHasChanged);
代码,这语法可以避免大量的回调代码,简化代码。
StateHasChanged
在一般情况下状态发生了改变,blazor
会自动更新绑定内容,但是如果在不同线程或者某些情况修改了状态,blazor
可能无法跟踪改变,导致界面没有刷新绑定内容,这时我们就可以使用StateHasChanged
方法显示的更新状态。
@inherits DrawerTemplate<TaskDto, TaskDto>
抽屉组件必须要继承DrawerTemplate
类,前面一个TaskDto
是抽屉打开时需要传入的参数类型,后面一个TaskDto
是抽屉关闭时返回的类型。
[Inject] public DrawerService DrawerSrv { get; set; }
依赖注入抽屉服务
Drawer组件帮助文档:https://antblazor.com/zh-CN/components/drawer
Form
表单组件Model="this.Options"
表单绑定的对象
Form组件帮助文档:https://antblazor.com/zh-CN/components/form
新增待办
要做的事情永远做不完,因为我们每天不停的在增加待办😣
<div class="task-input"> <DatePicker Picker="@DatePickerType.Date" @bind-Value="@newTask.PlanTime" /> <Input @bind-Value="@newTask.Title" OnkeyUp="OnInsert" /> </div>
TaskDto newTask = new TaskDto() { PlanTime = DateTime.Now.Date }; void OnInsert(KeyboardEventArgs e) { if (e.Code == "Enter") { taskDtos.Add(newTask); newTask = new TaskDto() { PlanTime = DateTime.Now.Date }; } }
将newTask
绑定到DatePicker
和Input
组件,然后注册OnkeyUp
事件,通过处理事件时采用if (e.Code == "Enter")
判断回车,当回车时将newTask加入taskDtos
集合,并创新新的newTask
用于下一次添加。
@bind-Value
双向绑定Value
属性,这个可以让组件中的数据更改和变量的值双向更新。
DatePicker
输入或选择日期的控件。Picker="@DatePickerType.Date"
日期选择模式
组件帮助文档:https://antblazor.com/zh-CN/components/datepicker
Input
通过鼠标或键盘输入内容,是最基础的表单域的包装。OnkeyUp="OnInsert"
键盘按键抬起事件,如果没有明确指定参数,那么他会带上KeyboardEventArgs
参数,不同的事件的参数不同,详见ASP.NET Core Blazor 事件处理
Input组件帮助文档:https://antblazor.com/zh-CN/components/input
删除待办
世上没有反悔药,但是程序的世界,反悔就是家常便饭,so,上代码
<span style="color:@(days switch { _ when days > 3 => "#ccc", _ when days > 0 => "#ffd800", _ => "#ff0000" })"> @item.Deadline?.ToShortDateString() </span> </div> <!--从这里开始插入以下代码--> <div class="del" @onclick="async e=>await OnDel(item)"> <Icon Type="rest" Theme="outline" /> </div>
[Inject] public ConfirmService ConfirmSrv { get; set; } public async Task OnDel(TaskDto task) { if (await ConfirmSrv.Show($"是否删除任务 {task.Title}", "删除", ConfirmButtons.YesNo, ConfirmIcon.Info) == ConfirmResult.Yes) { taskDtos.Remove(task); } }
这里使用ConfirmSrv
服务提供的消息框功能,并借助await
的特性,无需回调,直接判断返回值是否是ConfirmResult.Yes
,然后删除选择任务。
ConfirmSrv.Show
快捷地弹出一个内置的确认框。
modal组件帮助文档:https://antblazor.com/zh-CN/components/modal
完成待办
我的一天待办最后一个功能,完成它,gogogo
<Card Bordered="true" Size="small" Class="task-card"> <div class="task-card-item"> <!--从这里开始插入以下代码--> @{ var finishClass = new ClassMapper().Add("finish").If("unfinish", () => item.IsFinish == false); } <div class="@(finishClass.ToString())" @onclick="x => OnFinish(item)"> <Icon Type="check" Theme="outline" /> </div>
private void OnFinish(TaskDto task) { task.IsFinish = !task.IsFinish; }
这个功能的实现方式与“标记重要”功能相似,区别是它通过修改样式来显示与隐藏完成标记。
ClassMapper
类是AntDesignBlazor
中自带的class工具,它通过链式代码可以根据条件组合成需要的class .Add("finish")
添加名字为finish
的class .If("unfinish", () => item.IsFinish == false)
根据表达式item.IsFinish == false
值决定是否添加名字为unfinish
的class
全部待办
想要查看所有待办,那么就做一个“全部”界面,继续代码➡效果➡讲解三步走
创建TaskSearch.razor
文件
@page "/search" @inject TaskServices TaskSvr <PageHeader Title="@("全部待办事项")" Subtitle="@($"数量:{datas?.Count}")"></PageHeader> <Search @bind-Value="title" OnSearch="OnSearch"></Search> <Spin Spinning="@isLoading"> <Table DataSource="@datas"> <AntDesign.Column @bind-Field="@context.Title" Sortable> @context.Title @if (context.IsImportant) { <Tag Color="orange">重要</Tag> } </AntDesign.Column> <AntDesign.Column @bind-Field="@context.Description" /> <AntDesign.Column @bind-Field="@context.PlanTime" Sortable /> <AntDesign.Column @bind-Field="@context.Deadline" Sortable /> <AntDesign.Column @bind-Field="@context.IsFinish"> @if (context.IsFinish) { <Icon Type="check" Theme="outline" /> } </AntDesign.Column> </Table> </Spin>
private bool isLoading = false; protected async override Task OnInitializedAsync() { await base.OnInitializedAsync(); await OnSearch(); } private async Task OnSearch() { isLoading = true; datas = await TaskSvr.LoadSearch(title); isLoading = false; } private string title; List<TaskDto> datas = new List<TaskDto>();
在OnInitializedAsync
中使用OnSearch()
方法将数据载入datas
,界面使用Table
组件显示载入的数据。
Spin
用于页面和区块的加载中状态。Spinning="@isLoading"
设置加载状态。
Spin组件帮助文档:https://antblazor.com/zh-CN/components/spin
Table
展示行列数据DataSource="@datas"
表格中需要显示的数据通过DataSource
绑定
AntDesign.Column
表格中的列@bind-Field="@context.Title"
列显示的字段,支持模板
Table组件帮助文档:https://antblazor.com/zh-CN/components/table
Tag
进行标记和分类的小标签。Color="orange"
标签显示为橘色
Tag组件帮助文档:https://antblazor.com/zh-CN/components/tag
程序启动动画
因为WebAssembly
启动前需要一些时间下载代码,这个时候浏览器默认是白屏,这会让用户觉得网络不畅或者系统发生了问题,影响客户体验,所以我们通常会在启动时加入一个启动等待动画,这个只需要简单修改index.html
即可
<body> <app> <div class="loading"> <!--此处加入blazor完成启动前需要显示的载入动画--> <span></span> <span></span> <span></span> <span></span> <span></span> </div> </app> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div> <script src="_framework/blazor.webassembly.js"></script> </body> </html>
次回预告
到这里我们把待办工具的界面做好了,但是所有数据都是模拟的,下一次我们将通过HttpClient
实现前后端数据交互,以及使用EF Code
进行超级简单的数据库增删改查。