《进击吧!Blazor!》是自己与张善友老师单干的Blazor零根底入门系列视频,此系列能让一个从未接触过Blazor的程序员把握开发Blazor利用的能力。
视频地址:https://space.bilibili.com/48...
本系列文章是基于《进击吧!Blazor!》直播内容编写,降级.Net5,改良问题,解说更全面。因为篇幅无限,文章中省略了局部代码,残缺示例代码:https://github.com/TimChen44/...作者:陈超超
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; }}
<!--组件应用--><Component Title="Superior">
上例就是将Superior
通过参数传入组件,组件中就会输入Blazor is Superior!
路由参数
组件能够接管来自 @page
指令所提供的路由模板的路由参数。 路由器应用路由参数来填充相应的组件参数。参数类型受限于路由规定,只反对几个根本类型。
<!--页面代码-->@page "/RouteParameter/{text}"<h1>Blazor is @Text!</h1>@code { [Parameter] public string Text { get; set; }}
当应用/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"; }}
点击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"); }}
<!--父组件--><h1>Blazor is @Text!</h1><Component OnClick="OnClick"></Component>@code{ private string Text { get; set; } void OnClick(string e) { Text = e; }}
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(); } }
<!--组件应用--><Component @ref="@component"></Component><button @onclick="OnClick">Button</button>@code{ private Component component; void OnClick(MouseEventArgs e) { component.SetText("Superior"); }}
当点击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;}
把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;}
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; }}
5.嵌套
组件嵌套就是容许一个组件成为另一组件的容器,通过父与子的层层嵌套实现各种简单的界面,在这过程中咱们也能提炼出类似的组件,加以重复使用和共享。
上面是“我的一天”界面的代码以及他们组件的嵌套构造
子内容
组件能够设置本人的某一个地位插入其余组件的内容。
<!--组件代码--><h1>Blazor is @ChildContent</h1>@code{ [Parameter] public RenderFragment ChildContent { get; set; }}
<!--组件应用--><Component> <strong>Superior!</strong></Component>
Component
具备一个类型为 RenderFragment
的 ChildContent
属性,RenderFragment
示意要出现的 UI 段。ChildContent
的值是从父组件接管的UI段。
在组件中须要出现ChildContent
内容的中央搁置@ChildContent
标记。ChildContent
属性命名为固定名字,下例是残缺写法,下面是简略写法。
<Component> <ChildContent> <strong>Superior!</strong> </ChildContent></Component>
模板
能够通过指定一个或多个 RenderFragment
类型的组件参数来接管多个UI段。
<!--组件代码--><h1>@Title is @Quality</h1>@code{ [Parameter] public RenderFragment Title { get; set; } [Parameter] public RenderFragment Quality { get; set; }}
<!--组件应用--><Component> <Title> <strong>Blazor</strong> </Title> <Quality> <strong>Superior!</strong> </Quality></Component>
模板参数
能够定义 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; }}
<!--组件应用--><Component Items="items"> <Title Context="item"> <strong>@item</strong> </Title></Component>@code{ List<string> items = new List<string> { ".Net", "C#", "Blazor" };}
组件应用时通过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>
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>}
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($"题目必须填写"); return; } isNewLoading = true; var result = await Http.PostAsJsonAsync<TaskDto>($"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($"申请产生谬误 {result.StatusCode}"); } isNewLoading = false; StateHasChanged(); } }}
EventCallback<TaskDto> OnInserted
不同场景下插入后须要做的事件可能不同,所以通过这个事件由内部进行解决。Func<TaskDto> NewTaskFunc
不同场景下对TaskDto
初始化要求不同,所以用这个函数来调用初始化。RenderFragment<TaskDto> ChildContent
应用模板实现额定的表单进行扩大输出内容。
重要工作
创立Star.razor
文件作为重要工作
的页面文件,代码如下
@page "/star"<PageHeader Title="@("重要的工作")" Subtitle="@($"数量:{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>
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($"是否删除工作 {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>
C#代码因为变动很小,所以不再此处贴出
TaskItem
TitleTemplate
通过模板重写了题目的显示方式,反对当实现后题目减少删除线
NewTask
ChildContent
重写了子内容,提供了重要度的抉择。
次回预报
本人的待办当然只有本人能看了啦,所以登录,权限啥的都给安顿上,请关注下一节——平安