百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

《进击吧!Blazor!》第一章 5.组件开发

wptr33 2024-11-14 19:22 31 浏览

《进击吧!Blazor!》是本人与张善友老师合作的Blazor零基础入门系列视频,此系列能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力。
视频地址:https://space.bilibili.com/483888821/channel/detail?cid=151273
本系列文章是基于《进击吧!Blazor!》直播内容编写,升级.Net5,改进问题,讲解更全面。因为篇幅有限,文章中省略了部分代码,完整示例代码:https://github.com/TimChen44/Blazor-ToDo

作者:陈超超
Ant Design Blazor 项目贡献者,拥有十多年从业经验,长期基于.Net技术栈进行架构与开发产品的工作,现就职于正泰集团。
邮箱:timchen@live.com
欢迎各位读者有任何问题联系我,我们共同进步。

这次分享我么要聊聊Blazor的精髓,也是我个人认为Blazor框架体系中最优秀的特性——组件。

组件

组件(Component)是对数据和方法的简单封装。几乎所有UI相关的框架都有组件(控件)的概念。


早期的Delphi组件叫做VCL(Visual Component Library),它采用自身嵌套的方式组合成所需的用户界面,并提供属性,方法,事件与组件外部进行交互,自身有着独立的生命周期,在必要的时候进行销毁。

之后.Net的WinForms和WPF组件相对于Delphi虽然设计实现上完全不同,但是对组件的定义和用途上几乎一致。

现在Web前端框架Angular中也采用了组件的概念,整体理念依旧相似。

有些框架根据是否可见将组件分为,组件(Component)不可见,控件(Control)可见,比如Delphi,WinForms

纵观这些框架的组件设计,可以提炼出组件包含以下特性。


Blazor应用也是使用组件构建的。组件是自包含的用户界面 (UI) 块,例如页、对话框或窗体。 组件包含插入数据或响应 UI 事件所需的 HTML 标记和处理逻辑。 组件非常灵活且轻量。 可在项目之间嵌套、重复使用和共享。

1.参数(属性)

提供组件外部向组件内部传递数据的方式。

在Blazor中我们称组件的属性(Property)叫参数(Parameter),参数本身就是一个属性,但是为了让Blazor框架能区分两者,所以我们在属性上增加 [Parameter]特性来声明属性为组件的参数。

[Parameter]
public string Text { get; set; }

组件参数

组件参数可以接收来在razor页面中给与的值,支持简单类型,也可以支持复杂类型。

<!--组件代码-->
<h1>Blazor is @Text!</h1>
@code {
    [Parameter]
    public string Text { get; set; }
}
123456
<!--组件使用-->
<Component Title="Superior">
12

上例就是将Superior通过参数传入组件,组件中就会输出Blazor is Superior!

路由参数

组件可以接收来自 @page 指令所提供的路由模板的路由参数。 路由器使用路由参数来填充相应的组件参数。参数类型受限于路由规则,只支持几个基本类型。

<!--页面代码-->
@page "/RouteParameter/{text}"
<h1>Blazor is @Text!</h1>
@code {
    [Parameter]
    public string Text { get; set; }
}
1234567

当使用/RouteParameter/Superior地址进行路由时,跳转到上例中的页面,并且页面输出Blazor is Superior!

级联参数

在某些情况下,使用组件参数将数据从祖先组件流向子代组件不太方便,尤其是在有多个组件层时。 级联值和参数提供了一种方便的方法,使祖先组件为其所有子代组件提供值,从而解决了此问题。

祖先组件中使用CascadingValue设定需要向下传递的级联值,子代组件中使用 [CascadingParameter] 特性来声明级联参数用于接收级联值。

本文后续会有详细的Demo来讲解此特性,此处暂不展开了。

2.事件

事件是一种由组件内部发起,由组件外部处理的一种机制。

对于原始的Html元素与Razor组件在事件的使用上有一些细微差别,下面分开介绍。

Html 元素

对HTML 元素的事件采用@on{EVENT}格式(例如 @onclick)处理事件,Razor 组件将此属性的值视为事件处理程序。

<h1>Blazor is @Text!</h1>
<button @onclick="OnClick">Button</button>
@code
{
    private string Text { get; set; }
    void OnClick(MouseEventArgs e)
    {
        Text = "Superior";
    }
}
12345678910

点击Button按钮后就触发@onclick事件,然后设置Text的值,最后组件输出Blazor is Superior!
每一个事件都会返回一个参数,
@onclick事件返回MouseEventArgs参数,更多详见事件参数类型

Razor 组件

跨组件公开事件,可以使用 EventCallback。父组件可向子组件的 EventCallback 分配回调方法,由子组件完成调用。

<!--子组件-->
<button @onclick="OnBtnClick">Button</button>
@code {
    [Parameter]
    public EventCallback<string> OnClick { get; set; }

    void OnBtnClick(MouseEventArgs e)
    {
        if (OnClick.HasDelegate)
            OnClick.InvokeAsync("Superior");
    }
}
123456789101112
<!--父组件-->
<h1>Blazor is @Text!</h1>
<Component OnClick="OnClick"></Component>
@code
{
    private string Text { get; set; }
    void OnClick(string e)
    {
        Text = e;
    }
}
1234567891011


EventCallback<string> OnClick 定义了一个名为OnClick的事件,EventCallback的泛型参数就是事件的参数类型。
OnClick.InvokeAsync("Superior") 调用这个事件,让注册的方法执行,注意事件调用前通过OnClick.HasDelegate判断事件是否有被注册,如果没有任何方法注册此事件,那么调用会发生异常。
OnClick="OnClick"OnClick方法注册给事件。

3.方法

组件对外暴露的方法,提供外部组件调用。

<!--组件代码-->
<h1>Blazor is @Text!</h1>
@code
{ 
    private string Text { get; set; }
    public void SetText(string text)
    {
        Text = text;
        StateHasChanged();
    } 
}
1234567891011
<!--组件使用-->
<Component @ref="@component"></Component>
<button @onclick="OnClick">Button</button>
@code
{
    private Component component;
    void OnClick(MouseEventArgs e)
    {
        component.SetText("Superior");
    }
}
1234567891011

当点击Button按钮触发@onclick事件,通过Component组件的SetText方法设置组件的Text值,组件就输出Blazor is Superior!
@ref 想要获得某个组件的实例,可以使用@ref特性,在这里他会把Component组件的实例填充到component变量中。此处注意,@ref的应用只有在组件完成呈现后才完成。

4.数据绑定

参数只提供了外部组件向组件单向赋值,数据绑定就是双向赋值。

对于原始的Html元素与Razor组件在数据绑定的使用上有一些细微差别,下面分开介绍。

Html 元素

使用通过名为 @bind 的 Html 元素特性提供了数据绑定功能。

<h4>Blazor is @Text!</h4>
<input @bind="Text" />
@code
{
    private string Text;
}
123456


Text变量绑定到input组件,当input中完成输入且离开焦点后输出Blazor is Superior!

如果我们想要输入时立即显示输入的内容,我们可以通过带有 event 参数的 @bind:event 属性将绑定指向 oninput 事件。

<h4>Blazor is @Text!</h4>
<input @bind="Text" @bind:event="oninput"/>
@code
{
    private string Text;
}
123456


Html元素绑定实现原理
Html元素本身并不支持双向属性绑定机制,当我们使用
@bind后,Blazor帮我们生成了value="@Text"实现向Html元素赋值,再生成@onchange事件实现Html元素向绑定变量赋值。

<input value="@Text"
    @onchange="@((ChangeEventArgs __e) => Text = __e.Value.ToString())" />

@code {
    private string Text { get; set; }
}

1234567

5.嵌套

组件嵌套就是允许一个组件成为另一组件的容器,通过父与子的层层嵌套实现各种复杂的界面,在这过程中我们也能提炼出相似的组件,加以重复使用和共享。

下面是“我的一天”界面的代码以及他们组件的嵌套结构

子内容

组件可以设置自己的某一个位置插入其他组件的内容。

<!--组件代码-->
<h1>Blazor is @ChildContent</h1>
@code{
    [Parameter] public RenderFragment ChildContent { get; set; }
}
12345
<!--组件使用-->
<Component>
    <strong>Superior!</strong>
</Component>
1234


Component具有一个类型为 RenderFragmentChildContent 属性,RenderFragment表示要呈现的 UI 段。
ChildContent 的值是从父组件接收的UI段。
在组件中需要呈现
ChildContent内容的地方放置@ChildContent标记。
ChildContent属性命名为固定名字,下面是完整写法,上面是简略写法。

<Component>
    <ChildContent>
        <strong>Superior!</strong>
    </ChildContent>
</Component>
12345

模板

可以通过指定一个或多个 RenderFragment 类型的组件参数来接收多个UI段。

<!--组件代码-->
<h1>@Title is @Quality</h1>

@code{
    [Parameter] public RenderFragment Title { get; set; }
    [Parameter] public RenderFragment Quality { get; set; }
}
1234567
<!--组件使用-->
<Component>
    <Title>
        <strong>Blazor</strong>
    </Title>
    <Quality>
        <strong>Superior!</strong>
    </Quality>
</Component>
123456789

模板参数

可以定义 RenderFragment<TValue> 类型的组件参数来定义支持参数的模板。

<!--组件代码-->
@foreach (var item in Items)
{
    <h4>@Title(item) is Superior!</h4>
}
@code{
    [Parameter] public RenderFragment<string> Title { get; set; }
    [Parameter] public IReadOnlyList<string> Items { get; set; }
}
123456789
<!--组件使用-->
<Component Items="items">
    <Title Context="item">
        <strong>@item</strong>
    </Title>
</Component>
@code{
    List<string> items = new List<string> { ".Net", "C#", "Blazor" };
}
123456789


组件使用时通过
IReadOnlyList<string> Items属性将内容传入组件,组件内部使用@foreach (var item in Items)将集合循环呈现,@Title(item)确定了插入位置,且给模板传入item的值,再外部通过Context="item"接收参数,最终实现模板的呈现。

6.生命周期

Blazor 框架包括同步和异步生命周期方法。一般情况下同步方法会先与异步方法执行。
我们可以重写生命周期方法的,以在组件初始化和呈现期间对组件执行其他操作。

组件初始化

组件状态改变

组件销毁

ToDo应用组件化改造

任务信息

重要任务不论是否是今天,我们都需要便捷地查看,所以我们需要做一个“重要任务”的页面。
这个页面显示内容和“我的一天”非常相似,所以我们可以抽象出一个
TaskItem.razor组件,组件的Html以及样式基本是从ToDay.razor组件迁移过来。

<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="OnFinishClick">
            <Icon Type="check" Theme="outline" />
        </div>
        <div class="title" @onclick="OnCardClick">

            @if (TitleTemplate != null)
            {
                @TitleTemplate
            }
            else
            {
                <AntDesign.Text Strong> @Item.Title</AntDesign.Text>
                <br />
                <AntDesign.Text Type="@TextElementType.Secondary">
                    @Item.Description
                </AntDesign.Text>
            }
        </div>
        <div class="del" @onclick="OnDelClick">
            <Icon Type="rest" Theme="outline" />
        </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>
        @if (ShowStar)
        {
            <div class="star" @onclick="OnStarClick">
                <Icon Type="star" Theme="@(Item.IsImportant ? "fill" : "outline")" />
            </div>
        }
    </div>
</Card>
1234567891011121314151617181920212223242526272829303132333435363738394041424344
public partial class TaskItem
{
    //任务内容
    [Parameter] public TaskDto Item { get; set; }

    //完成图标事件
    [Parameter] public EventCallback<TaskDto> OnFinish { get; set; }
    public async void OnFinishClick()
    {
        if (OnFinish.HasDelegate)
            await OnFinish.InvokeAsync(Item);
    }

    //条目点击事件
    [Parameter] public EventCallback<TaskDto> OnCard { get; set; }
    public async void OnCardClick()
    {
        if (OnCard.HasDelegate)
            await OnCard.InvokeAsync(Item);
    }

    //删除图标事件
    [Parameter] public EventCallback<TaskDto> OnDel { get; set; }
    public async void OnDelClick()
    {
        if (OnDel.HasDelegate)
            await OnDel.InvokeAsync(Item);
    }

    //重要图标事件
    [Parameter] public EventCallback<TaskDto> OnStar { get; set; }
    public async void OnStarClick()
    {
        if (OnStar.HasDelegate)
            await OnStar.InvokeAsync(Item);
    }

    //是否相似重要图标
    [Parameter] public bool ShowStar { get; set; } = true;

    //支持标题模板
    [Parameter] public RenderFragment TitleTemplate { get; set; }
}

@if (TitleTemplate != null) 如果外部传入了模板,那么就是显示模板,否则就使用默认格式显示。

新建任务

在“重要任务”和“我的一天”中均有添加任务的功能,我们也将他们抽象成NewTask.razor组件。

<Divider Text="新任务"></Divider>
@if (newTask != null)
{
    <Spin Spinning="isNewLoading">
        <div class="task-input">
            <DatePicker Picker="@DatePickerType.Date" @bind-Value="@newTask.PlanTime" />
            <Input @bind-Value="@newTask.Title" OnkeyUp="OnInsertKey" />
            @if(ChildContent!=null )
            {
                @ChildContent(newTask)
            }
        </div>
    </Spin>
}
1234567891011121314
public partial class NewTask
{
    [Inject] public MessageService MsgSrv { get; set; }
    [Inject] public HttpClient Http { get; set; }

    [Parameter] public EventCallback<TaskDto> OnInserted { get; set; }
    [Parameter] public Func<TaskDto> NewTaskFunc { get; set; }
    [Parameter] public RenderFragment<TaskDto> ChildContent { get; set; }

    //新的任务
    TaskDto newTask { get; set; }
    private bool isNewLoading { get; set; }

    protected override void OnInitialized()
    {
        newTask = NewTaskFunc?.Invoke();
        base.OnInitialized();
    }

    async void OnInsertKey(KeyboardEventArgs e)
    {
        if (e.Code == "Enter")
        {
            if (string.IsNullOrWhiteSpace(newTask.Title))
            {
                MsgSrv.Error(#34;标题必须填写");
                return;
            }
            isNewLoading = true;
            var result = await Http.PostAsJsonAsync<TaskDto>(#34;api/Task/SaveTask", newTask);
            if (result.IsSuccessStatusCode)
            {
                newTask.TaskId = await result.Content.ReadFromJsonAsync<Guid>();
                await Task.Delay(1000);
                if (OnInserted.HasDelegate) await OnInserted.InvokeAsync(newTask);

                newTask = NewTaskFunc?.Invoke();
            }
            else
            {
                MsgSrv.Error(#34;请求发生错误 {result.StatusCode}");
            }
            isNewLoading = false;
            StateHasChanged();
        }
    }
}

EventCallback<TaskDto> OnInserted 不同场景下插入后需要做的事情可能不同,所以通过这个事件由外部进行处理。
Func<TaskDto> NewTaskFunc 不同场景下对TaskDto初始化要求不同,所以用这个函数来调用初始化。
RenderFragment<TaskDto> ChildContent 使用模板实现额外的表单进行扩展输入内容。

重要任务

创建Star.razor文件作为重要任务的页面文件,代码如下

@page "/star"

<PageHeader Title="@("重要的任务")" Subtitle="@(#34;数量:{taskDtos?.Count}")"></PageHeader>

<Spin Spinning="@isLoading">
    @foreach (var item in taskDtos)
    {
        <TaskItem  Item="item" OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar" ShowStar="false">
        </TaskItem>
    }
    <NewTask OnInserted="OnInsert" NewTaskFunc="() => new TaskDto() { PlanTime = DateTime.Now.Date, IsImportant = true  }"></NewTask>
</Spin>
123456789101112
public partial class Star
{
    // 1、	列出当天的所有代办工作
    [Inject] public HttpClient Http { get; set; }
    
    bool isLoading = true;
    private List<TaskDto> taskDtos = new List<TaskDto>();
    protected async override Task OnInitializedAsync()
    {
        isLoading = true;
        taskDtos = await Http.GetFromJsonAsync<List<TaskDto>>("api/Task/GetStarTask");
        isLoading = false;
        await base.OnInitializedAsync();
    }

    //2、	添加代办
    public MessageService MsgSrv { get; set; }
    async void OnInsert(TaskDto item)
    {
        taskDtos.Add(item);
    }

    //3、	编辑抽屉
    [Inject] public TaskDetailServices TaskSrv { get; set; }
    async void OnCardClick(TaskDto task)
    {
        TaskSrv.EditTask(task, taskDtos);
        await InvokeAsync(StateHasChanged);
    }

    //4、	修改重要程度
    private async void OnStar(TaskDto task)
    {
        var req = new SetImportantReq()
        {
            TaskId = task.TaskId,
            IsImportant = !task.IsImportant,
        };

        var result = await Http.PostAsJsonAsync<SetImportantReq>("api/Task/SetImportant", req);
        if (result.IsSuccessStatusCode)
        {
            task.IsImportant = req.IsImportant;
            StateHasChanged();
        }
    }

    //5、	修改完成与否
    private async void OnFinish(TaskDto task)
    {
        var req = new SetFinishReq()
        {
            TaskId = task.TaskId,
            IsFinish = !task.IsFinish,
        };

        var result = await Http.PostAsJsonAsync<SetFinishReq>("api/Task/SetFinish", req);
        if (result.IsSuccessStatusCode)
        {
            task.IsFinish = req.IsFinish;
            StateHasChanged();
        }
    }

    //6、	删除代办
    [Inject] public ConfirmService ConfirmSrv { get; set; }

    public async Task OnDel(TaskDto task)
    {
        if (await ConfirmSrv.Show(#34;是否删除任务 {task.Title}", "删除", ConfirmButtons.YesNo, ConfirmIcon.Info) == ConfirmResult.Yes)
        {
            taskDtos.Remove(task);
        }
    }
}


TaskItem
OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar" 绑定不同的操作函数

此处完全可以使用上一节介绍服务将这些方法提取到一个独立的服务中,这里我就偷懒不改了。

ShowStar="false" 不显示重要图标

NewTask
NewTaskFunc="() => new TaskDto() { PlanTime = DateTime.Now.Date, IsImportant = true }" 重要初始化时默认将IsImportant设置成true

我的一天

我们将“我的一天”也进行适当改造

@page "/today"

<PageHeader Title="@("我的一天")" Subtitle="@DateTime.Now.ToString("yyyy年MM月dd日")"></PageHeader>

<Spin Spinning="@isLoading">
    @foreach (var item in taskDtos)
    {
        <TaskItem @key="item.TaskId" Item="item" OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar">
            <TitleTemplate>
                <AntDesign.Text Strong Style="@(item.IsFinish?"text-decoration: line-through;color:silver;":"")"> @item.Title</AntDesign.Text>
                <br />
                <AntDesign.Text Type="@TextElementType.Secondary">
                    @item.Description
                </AntDesign.Text>
            </TitleTemplate>
        </TaskItem>
    }

    <NewTask OnInserted="OnInsert" NewTaskFunc="()=>  new TaskDto() {PlanTime=DateTime.Now.Date }">
        <ChildContent Context="newTask">
            <RadioGroup @bind-Value="newTask.IsImportant">
                <Radio RadioButton Value="true">重要</Radio>
                <Radio RadioButton Value="false">普通</Radio>
            </RadioGroup>
        </ChildContent>
    </NewTask>
</Spin>
123456789101112131415161718192021222324252627

C#代码因为变化很小,所以不在此处贴出


TaskItem
TitleTemplate 通过模板重写了标题的显示方式,支持当完成后标题增加删除线

NewTask
ChildContent 重写了子内容,提供了重要度的选择。

次回预告

自己的待办当然只有自己能看了啦,所以登录,权限啥的都给安排上,请关注下一节——安全

学习资料

更多关于Blazor学习资料:https://aka.ms/LearnBlazor

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...