关于c#:进击吧Blazor第一章-5组件开发

52次阅读

共计 11768 个字符,预计需要花费 30 分钟才能阅读完成。

《进击吧!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具备一个类型为 RenderFragmentChildContent 属性,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 重写了子内容,提供了重要度的抉择。

次回预报

本人的待办当然只有本人能看了啦,所以登录,权限啥的都给安顿上,请关注下一节——平安

学习材料

更多对于 Blazor 学习材料:https://aka.ms/LearnBlazor

正文完
 0