《进击吧!Blazor!》第一章 3.页面制作

分布式kv存储系统之Etcd集群

作者介绍

陈超超
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的开发。

准备工作

项目准备

  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 项目无缝集成。

项目地址:https://github.com/ant-design-blazor/ant-design-blazor
文档地址:https://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组件帮助文档: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作为参数传入,方法中修改了TaskDtoIsImportant属性值,通过@(item.IsImportant ? "fill" : "outline")单向绑定,实现修改Icon组件的Theme样式在filloutline切换。

@()相比@标记,它可以在()括号中使用单行代码进行单向绑定。
@onclick事件绑定,除了onclick还有很多,详见ASP.NET Core Blazor事件处理

Icon语义化的矢量图形。
Type="star"图标名称

Icon组件帮助文档:https://antblazor.com/zh-CN/components/icon

libuv事件循环中的三种句柄

计划时间

既然是待办,那么必然有一个计划开始时间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绑定到DatePickerInput组件,然后注册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")添加名字为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组件帮助文档: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进行超级简单的数据库增删改查。

 

学习资料:

https://aka.ms/LearnBlazor

OPC UA 统一架构 (二)

相关推荐

发表评论

路人甲

网友评论(0)