作者介紹
陳超超
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
進行超級簡單的數據庫增刪改查。