《進擊吧!Blazor!》第一章 5.組件開發

《進擊吧!Blazor!》是本人與張善友老師合做的Blazor零基礎入門系列視頻,此係列能讓一個從未接觸過Blazor的程序員掌握開發Blazor應用的能力。
視頻地址: https://space.bilibili.com/48...
本系列文章是基於《進擊吧!Blazor!》直播內容編寫,升級.Net5,改進問題,講解更全面。由於篇幅有限,文章中省略了部分代碼,完整示例代碼: https://github.com/TimChen44/...

做者:陳超超
Ant Design Blazor 項目貢獻者,擁有十多年從業經驗,長期基於.Net技術棧進行架構與開發產品的工做,現就任於正泰集團。
郵箱:timchen@live.com
歡迎各位讀者有任何問題聯繫我,咱們共同進步。html

此次分享我麼要聊聊Blazor的精髓,也是我我的認爲Blazor框架體系中最優秀的特性——組件。前端

組件

組件(Component)是對數據和方法的簡單封裝。幾乎全部UI相關的框架都有組件(控件)的概念。git

在這裏插入圖片描述
早期的Delphi組件叫作VCL(Visual Component Library),它採用自身嵌套的方式組合成所需的用戶界面,並提供屬性,方法,事件與組件外部進行交互,自身有着獨立的生命週期,在必要的時候進行銷燬。 程序員

以後.Net的WinForms和WPF組件相對於Delphi雖然設計實現上徹底不一樣,可是對組件的定義和用途上幾乎一致。github

如今Web前端框架Angular中也採用了組件的概念,總體理念依舊類似。api

有些框架根據是否可見將組件分爲,組件(Component)不可見,控件(Control)可見,好比Delphi,WinForms

縱觀這些框架的組件設計,能夠提煉出組件包含如下特性。
在這裏插入圖片描述
Blazor應用也是使用組件構建的。組件是自包含的用戶界面 (UI) 塊,例如頁、對話框或窗體。 組件包含插入數據或響應 UI 事件所需的 HTML 標記和處理邏輯。 組件很是靈活且輕量。 可在項目之間嵌套、重複使用和共享。安全

1.參數(屬性)

提供組件外部向組件內部傳遞數據的方式。前端框架

在Blazor中咱們稱組件的屬性(Property)叫參數(Parameter),參數自己就是一個屬性,可是爲了讓Blazor框架能區分二者,因此咱們在屬性上增長 [Parameter]特性來聲明屬性爲組件的參數。架構

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

組件參數

組件參數能夠接收來在razor頁面中給與的值,支持簡單類型,也能夠支持複雜類型。app

<!--組件代碼-->
<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
{
    // 一、    列出當天的全部代辦工做
    [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();
    }

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

    //三、    編輯抽屜
    [Inject] public TaskDetailServices TaskSrv { get; set; }
    async void OnCardClick(TaskDto task)
    {
        TaskSrv.EditTask(task, taskDtos);
        await InvokeAsync(StateHasChanged);
    }

    //四、    修改重要程度
    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();
        }
    }

    //五、    修改完成與否
    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();
        }
    }

    //六、    刪除代辦
    [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

相關文章
相關標籤/搜索