《進擊吧!Blazor!》第一章 3.頁面製作

作者介紹

在這裡插入圖片描述

陳超超
Ant Design Blazor 項目貢獻者
擁有十多年從業經驗,長期基於.Net技術棧進行架構與開發產品的工作,Ant Design Blazor 項目貢獻者,現就職於正泰集團

 

寫專欄開頭老規矩了,所以……先來段廣告😁
《進擊吧!Blazor!》是本人與張善友老師合作的Blazor零基礎入門系列視頻,此系列能讓一個從未接觸過Blazor的程序員掌握開發Blazor應用的能力。
視頻地址://space.bilibili.com/483888821/channel/detail?cid=151273
演示代碼://github.com/TimChen44/Blazor-ToDo
本系列文章是基於《進擊吧!Blazor!》直播內容編寫,升級.Net5,改進問題,講解更全面。

從這次分享開始我通過製作一個ToDo應用來介紹Balzor的開發。

準備工作

項目準備

  1. 打開上一次分享內容創建項目
    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;
}
  1. 修改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 項目無縫集成。

項目地址://github.com/ant-design-blazor/ant-design-blazor
文檔地址://antblazor.com/

安裝

  1. 用NuGet安裝AntDesign包
 Install-Package AntDesign -Version 0.5.3 
  1. 在 Program.cs 中註冊:
public static async Task Main(string[] args)
{
    //其他代碼
    builder.Services.AddAntDesign();
    await builder.Build().RunAsync();
}
  1. 在 wwwroot/index.html 中引入靜態文件:
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet">
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
  1. 在 _Imports.razor 中加入命名空間
 @using AntDesign 
  1. 為了動態地顯示彈出組件,需要在 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組件幫助文檔://antblazor.com/zh-CN/components/layout

Menu 菜單組件
Theme=”MenuTheme.Dark”黑色主題

Menu組件幫助文檔://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組件幫助文檔://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組件幫助文檔://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組件幫助文檔://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作為參數傳入,方法中修改了TaskDtoIsImportant屬性值,通過@(item.IsImportant ? "fill" : "outline")單向綁定,實現修改Icon組件的Theme樣式在filloutline切換。

@()相比@標記,它可以在()括號中使用單行代碼進行單向綁定。
@onclick事件綁定,除了onclick還有很多,詳見ASP.NET Core Blazor事件處理

Icon語義化的矢量圖形。
Type="star"圖標名稱

Icon組件幫助文檔://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組件幫助文檔://antblazor.com/zh-CN/components/drawer

Form表單組件
Model="this.Options"表單綁定的對象

Form組件幫助文檔://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綁定到DatePickerInput組件,然後註冊OnkeyUp事件,通過處理事件時採用if (e.Code == "Enter")判斷回車,當回車時將newTask加入taskDtos集合,並創新新的newTask用於下一次添加。

@bind-Value雙向綁定Value屬性,這個可以讓組件中的數據更改和變量的值雙向更新。

DatePicker輸入或選擇日期的控件。
Picker="@DatePickerType.Date"日期選擇模式

組件幫助文檔://antblazor.com/zh-CN/components/datepicker

Input通過鼠標或鍵盤輸入內容,是最基礎的表單域的包裝。
OnkeyUp="OnInsert"鍵盤按鍵抬起事件,如果沒有明確指定參數,那麼他會帶上KeyboardEventArgs參數,不同的事件的參數不同,詳見ASP.NET Core Blazor 事件處理

Input組件幫助文檔://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組件幫助文檔://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")添加名字為finishclass .If("unfinish", () => item.IsFinish == false)根據表達式item.IsFinish == false值決定是否添加名字為unfinishclass

全部待辦

想要查看所有待辦,那麼就做一個「全部」界面,繼續代碼➡效果➡講解三步走
創建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組件幫助文檔://antblazor.com/zh-CN/components/spin

Table展示行列數據
DataSource="@datas"表格中需要顯示的數據通過DataSource綁定

AntDesign.Column表格中的列
@bind-Field="@context.Title"列顯示的字段,支持模板

Table組件幫助文檔://antblazor.com/zh-CN/components/table

Tag進行標記和分類的小標籤。
Color="orange"標籤顯示為橘色

Tag組件幫助文檔://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進行超級簡單的數據庫增刪改查。

 

學習資料:

//aka.ms/LearnBlazor