2021年7月2日 星期五

Blazor Server 必會開發技能 - 元件生命週期事件

Blazor Server 必會開發技能 - 元件生命週期事件


Blazor Server 必會開發技能

Blazor Server 建立一個新的頁面

Blazor Server 元件生命週期事件 Component Life Cycle

Blazor Server C# 程式碼設計方法

Blazor Server 單向資料綁定 One Way Data Binding 與 重新轉譯 Binding

Blazor Server Hello 互動頁面與事件設計 Two Way Data Binding

Blazor Server 元件間的參數傳遞與回應事件 Component Parameter EventCallback

Blazor Server C# 與 JavaScript 互相呼叫 IJSRuntime

Blazor Server 表單和驗證 Form Validation 


在進行 Blazor 專案開發的時候,會設計出許多的 Razor Component 元件 ( 也可以稱為 Blazor 元件),並且將這些元件組合起來,便可以設計出相當優秀的 Web 頁面專案。

當使用預設的 Blazor Server 專案,每個頁面要顯示在網頁的時候,都會套用 [MainLayout.razor] Razor 元件經過 Render 轉譯的處理動作,便可以在瀏覽器上看到這個網頁內容,這裡可以從 [Pages] > [MainLayout.razor] 這個 Razor 元件檔案內容看出來。

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <div class="content px-4">
            @Body
        </div>
    </div>
</div>

這個檔案將會在這個 Blazor 專案內用於版面配置的目的,也就是每個有使用 @page ... 這樣的 指示詞 Directive 的 Razor 元件,可以直接透過在瀏覽器上輸入適當的 URL,便可以顯示這個頁面;而所指定的頁面將會顯示在上面 [MainLayout.razor] 檔案內的 @Body 區域內。

另外,在這個 Razor 元件內也有一個 <NavMenu /> ,這個元件將會用來顯示在網頁最左邊的功能選項清單之用,也就是說,最佳的 Blazor 專案設計策略,將會是將許多網頁上的區塊內容,切割成為不同的 Razor 元件,接著在進行組合成為一個複合式 Razor 元件,接著,就可以將這個複合式元件顯示到瀏覽器畫面上,如此,使用者就可以最終看到這個網頁內容。

對於在使用 Razor 元件進行程式設計的時候,將會需要了解到 Razor 元件的生命週期,也就是要來了解當 Razor 元件誕生之後,緊接著會觸發那些委派方法事件,如此,程式設計師便可以透過這些委派事件進行額外的商業邏輯設計。

例如:當這個元件建立之後,期望能夠設定一些預設值、從資料庫內讀需必要的紀錄並顯示到螢幕上等等需求,就需要使用到這些委派方法事件來做到,而在這些委派方法事件會在甚麼時候被觸發與這些委派方法事件的執行先後順序為何,便是在這篇文章可以來了解。

這裡說明的範例專案原始碼位於 BS02

建立 Blazor Server-Side 的專案

  • 打開 Visual Studio 2019

  • 點選右下方的 [建立新的專案] 按鈕

  • [建立新專案] 對話窗將會顯示在螢幕上

  • 從[建立新專案] 對話窗的中間區域,找到 [Blazor 應用程式] 這個專案樣板選項,並且選擇這個項目

  • 點選右下角的 [下一步] 按鈕

  • 現在 [設定新的專案] 對話窗將會出現

  • 請在這個對話窗內,輸入適當的 [專案名稱] 、 [位置] 、 [解決方案名稱]

    在這裡請輸入 [專案名稱] 為 BS02

  • 完成後,請點選 [建立] 按鈕

  • 當出現 [建立新的 Blazor 應用程式] 對話窗的時候

  • 請選擇最新版本的 .NET Core 與 [Blazor 伺服器應用程式]

  • 完成後,請點選 [建立] 按鈕

    稍微等會一段時間,Blazor 專案將會建立起來

建立 Razor 元件

  • 滑鼠右擊 Blazor 專案內的 [Pages] 資料夾
  • 選擇 [加入] > [Razor 元件]
  • 當 [新增項目 - BS02] 對話窗出現之後,請在下方名稱欄位內,輸入 LifeCycleView
  • 最後點選 [新增] 按鈕
  • 請依據底下程式碼替換到這個檔案內容
<div class="card">
    <h5 class="card-header">LifeCycleView.razor</h5>
    <div class="card-body">
        <h3 class="text-primary">@Message</h3>
    </div>
</div>

@code {
    [Parameter]
    public string Message { get; set; }

    protected override void OnInitialized()
    {
        Console.WriteLine($"執行 OnInitialized");
    }
    protected override Task OnInitializedAsync()
    {
        Console.WriteLine($"執行 OnInitializedAsync");
        return Task.FromResult(0);
    }
    protected override void OnAfterRender(bool firstRender)
    {
        Console.WriteLine($"執行 OnAfterRender - {firstRender}");
    }
    protected override Task OnAfterRenderAsync(bool firstRender)
    {
        Console.WriteLine($"執行 OnAfterRenderAsync - {firstRender}");
        return Task.FromResult(0);
    }
    protected override void OnParametersSet()
    {
        Console.WriteLine($"執行 OnParametersSet");
    }
    protected override Task OnParametersSetAsync()
    {
        Console.WriteLine($"執行 OnParametersSetAsync");
        return Task.FromResult(0);
    }
    public override Task SetParametersAsync(ParameterView parameters)
    {
        Console.WriteLine($"執行 SetParametersAsync");
        return base.SetParametersAsync(parameters);
    }
    protected override bool ShouldRender()
    {
        Console.WriteLine($"執行 ShouldRender");
        return base.ShouldRender();
    }
}

在這個 [Razor 元件] 檔案中,僅有六行 HTML 標記的宣告,這裡使用 Bootstrap Card 的方式來呈現這個元件要顯示在網頁上的方式,並且在 Card 裡面顯示出這個元件參數內容,也就是 @Message 這個變數。

在底下 C# 程式碼中,將會使愈 C# 屬性 Attribute [Parameter] 來標示出 [Message] 這個 C# 類別內的 屬性 Property 是具備這個元件的參數,也就是說,當在使用 [LifeCycleView] 元件時候,可以透過這個使用 HTML 標記屬性方式,傳遞相關物件值到這個元件內的 [Message] 變數內。

由於每個 [Razor 元件] 都會由編譯器產生為一個類別,而這些類別將會繼承 [ComponentBase] 元件的選擇性基底類別,例如,在編譯器對於這個元件將會產生出這樣的類別宣告 : public partial class LifeCycleView : Microsoft.AspNetCore.Components.ComponentBase

在 [ComponentBase] 類別內,將會有宣告了這些 OnInitialized 、 OnParametersSet 、 OnAfterRender 、 SetParametersAsync 同步的生命週期事件與這些 OnInitializedAsync 、 OnParametersSetAsync 、 OnAfterRenderAsync 、 ShouldRender 非同步的生命週期事件;而當在使用這些 Blazor 元件的時候,將會依序觸發這些生命週期事件

  • 第一次使用這個 Razor 元件時候,會先建立這個元件之執行個體
  • 當在第一次在要求時轉譯元件,將會觸發底下這兩個生命週期事件
  • 執行 SetParametersAsync 方法來執行屬性注入,這代表在轉譯樹狀結構中設定元件父系所提供的參數
  • 執行 OnInitialized 、 OnInitializedAsync ,代表元件準備好啟動時叫用的方法,在轉譯樹狀結構中從其父系收到其初始參數。
  • 因為剛剛已經執行完成了 SetParametersAsync 方法,所以將會觸發 OnParametersSet 、 OnParametersSetAsync ,這兩個方法表示當元件在轉譯樹狀結構中從其父系收到參數,且傳入值已指派給屬性時,將會觸發這兩個方法
  • OnAfterRender 、 OnAfterRenderAsync 這表示每次轉譯元件之後叫用的方法
  • ShouldRender 方法將會用於傳回布林值,指出元件是否應該要進行轉譯處理動作

在這個元件內將會覆寫上述的相關的生命週期事件方法,並且在這些事件方法內,使用 Console.WriteLine 方法顯示出一段文字,說明現在正在執行這個事件方法。

使用這個元件

  • 打開 [Pages] 資料夾內的 [Index.razor] 檔案
  • 請使用底下程式碼替換到這個檔案內容
@page "/"

<h1>Hello, Blazor 生命週期事件!</h1>

<LifeCycleView Message="@MyMessage" />
<br />
<button class="btn btn-primary"
        @onclick="OnClick">
    進行資料綁定變更
</button>

<SurveyPrompt Title="How is Blazor working for you?" />

@code{
    public string MyMessage { get; set; } = "First";

    void OnClick()
    {
        Console.WriteLine();
        MyMessage = "文字變更了";
    }
}

當要使用一個 [Razor 元件] 或者 [Blazor 元件] 的時候,只需要使用這個元件的名稱當作為一個 HTML 標籤來使用即可,若想要傳遞引數到這個元件內,在這個元件標籤旁,標示這個參數的屬性名稱與要傳遞過去的物件值即可,如同上面程式碼使用方式 <LifeCycleView Message="@MyMessage" />

在這個首頁元件內,另外宣告一個按鈕,當按下這個按鈕之後,將會變更所綁定的參數值。

執行這個專案

  • 使用 Kestrel 的方式來執行這個專案

  • 按下 [F5] 按鍵,開始執行這個 Blazor 專案

  • 一旦啟動完成,就會自動開以瀏覽器

  • 此時,從 [命令提示字元視窗] 內將會看到底下的內容輸出

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Vulcan\GitHub\Blazor-Xamarin-Full-Stack-HOL\Examples\BS02\BS02
執行 SetParametersAsync
執行 OnInitialized
執行 OnInitializedAsync
執行 OnParametersSet
執行 OnParametersSetAsync
執行 SetParametersAsync
執行 OnInitialized
執行 OnInitializedAsync
執行 OnParametersSet
執行 OnParametersSetAsync
執行 OnAfterRender - True
執行 OnAfterRenderAsync - True執行結果
  • 在這裡將會看到 OnInitialized 這個方法有執行兩次,這是因為這個 Blazor 專案有宣告使用 ServerPrerendered ,這表示將元件轉譯為靜態 HTML,並包含 Blazor 伺服器端應用程式的標記,而第二次的轉譯行為,才是真正要輸出到瀏覽器的 DOM 物件清單內,如此才會讓使用者看到最後的網頁結果,這裡可以從在最後面才會有呼叫 OnAfterRender 來看出來
  • 接著,點選 [進行資料綁定變更] 按鈕
  • 此時當初所綁定的參數物件值變更了,所以觸發了 SetParametersAsync
  • 當參數綁定行為處理完畢之後,將會觸發 OnParametersSet
  • 接下來觸發 ShouldRender 方法,決定是否需要轉譯這次變更的網頁內容
  • 一旦轉譯完成,將會觸發 OnAfterRender 方法
  • 底下為按下這個按鈕的輸出內容
執行 SetParametersAsync
執行 OnParametersSet
執行 OnParametersSetAsync
執行 ShouldRender
執行 OnAfterRender - False 

執行 OnAfterRenderAsync - False 














2021年7月1日 星期四

使用 Ant Design Blazor 設計出 CRUD 應用元件

使用 Ant Design Blazor 設計 CRUD 應用元件

在之前的文章 如何使用 Ant Design Blazor ,說明如何在 Blazor 專案內來使用 [Ant Design of Blazor] 這個元件的開發過程。

在這篇文章將會使用 Ant Design of Blazor 這套元件內的 Table 這個 Blazor 元件,並且用這個元件來設計出具有 CRUD (Create, Retrive, Update, Delete) 新增、查詢、更新、刪除 四大功能,而這樣的需求也是要學會開發 Web 專案技術的一個必備技能。

這篇文章的原始碼位於 bzAntTable

建立 Blazor Server-Side 的專案

  • 開啟 Visual Studio 2019
  • 選擇右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗中
  • 從右上方的專案類型下拉按鈕中,找到並選擇 [Web]
  • 從可用專案範本清單內,找到並選擇 [Blazor Server 應用程式]
  • 點選左下方 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗中
  • 在 [專案名稱] 欄位中輸入 bzAntTable
  • 點選左下方 [下一步] 按鈕
  • 在 [其他資訊] 對話窗中
  • 在 [目標 Framework] 下拉選單中,選擇 [.NET 5.0 (目前)]
  • 點選左下方 [建立] 按鈕

加入所需要使用到的 NuGet 套件

  • 滑鼠右擊 [bzAntTable] 專案內的 [相依性] 節點
  • 從彈出功能表中,選擇 [管理 NuGet 套件]
  • 當 [NuGet: bzAntTable] 視窗出現後,切換到 [瀏覽] 標籤頁次
  • 搜尋 [AntDesign] 並且安裝並且套件

安裝與設定 AntDesign 元件

  • 參考 Import Ant Design Blazor into an existing project 文件說明,準備開始進行這個元件的安裝與設定
  • 打開這個專案根目錄下的 [Startup.cs] 檔案
  • 找到這個方法 [ConfigureServices]
  • 在其方法內加入底下程式碼
#region 加入 Ant Design 會用到的相依性服務註冊
services.AddAntDesign();
#endregion
  • 打開 [Pages] 資料夾下的 [_Host.cshtml] 檔案
  • 在 <head> ... </head> 區段內加入底下 HTML 標記
@*加入 Ant Design 會用到的靜態參考*@
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
  • 打開這個專案根目錄下的 [_Imports.razor] 檔案
  • 在其檔案的最後面加入底下程式碼
@using AntDesign
  • 打開這個專案根目錄下的 [App.razor] 檔案
  • 在其檔案的最後面加入底下程式碼
<AntContainer /> 

建立資料模型與服務

  • 滑鼠右擊 [Pages] 資料夾
  • 從彈出功能表點選 [加入] > [類別]
  • 在下方 [名稱] 欄位輸入 [MyNote.cs]
  • 點選 [新增] 按鈕
  • 使用底下程式碼替換這個檔案內容
using System;
using System.ComponentModel.DataAnnotations;

namespace bzAntTable.Pages
{
    public class MyNote : ICloneable
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "事項標題不可為空白")]
        public string Title { get; set; }
        // 使用淺層複製的方式,產生出相同屬性值的物件
        public MyNote Clone()
        {
            return ((ICloneable)this).Clone() as MyNote;
        }
        // 這裡為使用明確方式來實作 ICloneable 介面
        object ICloneable.Clone()
        {
            return this.MemberwiseClone();
        }
    }
}
  • 滑鼠右擊 [Pages] 資料夾
  • 從彈出功能表點選 [加入] > [類別]
  • 在下方 [名稱] 欄位輸入 [MyNoteService.cs]
  • 點選 [新增] 按鈕
  • 使用底下程式碼替換這個檔案內容
using System;
using System.Collections.Generic;
using System.Linq;

namespace bzAntTable.Pages
{
    public class MyNoteService
    {
        public List<MyNote> MyNoteItems { get; set; }
        public MyNoteService()
        {
            var rnd = new Random();
            MyNoteItems = Enumerable.Range(1, 88).Select(index =>
            {
                var temperatureC = rnd.Next(-20, 55);
                return new MyNote
                {
                    Id = index,
                    Title=$"我的記事 {index}",
                };
            }).ToList();
        }

        public (List<MyNote> items, int total) Get(int pageIndex, int pageSize)
        {
            var items = MyNoteItems
                .Skip((pageIndex - 1)* pageSize)
                .Take(pageSize)
                .ToList();
            var total = MyNoteItems.Count;
            return (items, total);
        }
        public void Add(MyNote myNote)
        {
            var rnd = new Random();
            myNote.Id = rnd.Next(1000, 9999999);
            var item = MyNoteItems.FirstOrDefault(x => x.Id == myNote.Id);
            if (item == null)
            {
                MyNoteItems.Add(myNote);
            }
        }
        public void Update(MyNote myNote)
        {
            var item = MyNoteItems.FirstOrDefault(x => x.Id == myNote.Id);
            if (item != null)
            {
                item.Title = myNote.Title;
            }
        }
        public void Delete(MyNote myNote)
        {
            MyNoteItems.Remove(MyNoteItems.FirstOrDefault(x => x.Id == myNote.Id));
        }
    }
}

建立一個 CRUD 的元件

  • 滑鼠右擊 [Pages] 資料夾
  • 從彈出功能表點選 [加入] > [Razor 元件]
  • 在下方 [名稱] 欄位輸入 [MyNoteView.razor]
  • 點選 [新增] 按鈕
  • 使用底下程式碼替換這個檔案內容
@inject MyNoteService MyNoteService

<Button @onclick="AddAsync">新增</Button>

<div>
    <Table @ref="table"
           TItem="MyNote"
           DataSource="@myNotes"
           Total="_total"
           @bind-PageIndex="_pageIndex"
           @bind-PageSize="_pageSize"
           @bind-SelectedRows="selectedRows"
           OnPageIndexChange="PageIndexChanged"
           OnPageSizeChange="PageSizeChange">
        <Column @bind-Field="@context.Id" Title="代號" />
        <Column @bind-Field="@context.Title" Title="名稱" />
        <ActionColumn Width="100" Fixed="right">
            <Space>
                <SpaceItem><Button OnClick="async ()=>await EditAsync(context)">Edit</Button></SpaceItem>
                <SpaceItem><Button Danger OnClick="async ()=>await DeleteAsync(context)">Delete</Button></SpaceItem>
            </Space>
        </ActionColumn>
    </Table>
</div>

<div>
    @{
        RenderFragment footer =
        @<Template>
            <Button OnClick="@HandleCancel">
                關閉
            </Button>
        </Template>;
    }
    <Modal Title="@title"
           Visible="@visible"
           CancelText="@cancelText"
           OnCancel="@HandleCancel"
           Footer="@footer">

        <Form Model="CurrentMyNote"
              OnFinish="OnFinishAsync">
            <FormItem Label="記事名稱">
                <Input Placeholder="Basic usage" @bind-Value="@CurrentMyNote.Title" />
            </FormItem>
            <FormItem WrapperColOffset="8" WrapperColSpan="16">
                <Button Type="@ButtonType.Primary" HtmlType="submit">
                    儲存
                </Button>
            </FormItem>
        </Form>


    </Modal>
</div>

@using System.Text.Json;
@code {

    List<MyNote> myNotes;
    MyNote CurrentMyNote;
    public bool NewRecordMode { get; set; }

    IEnumerable<MyNote> selectedRows;
    ITable table;

    int _pageIndex = 1;
    int _pageSize = 5;
    int _total = 0;

    #region 編輯記錄對話窗
    string title = "編輯紀錄";
    bool visible = false;
    string cancelText = "關閉";
    #endregion

    protected override async Task OnInitializedAsync()
    {
        await ReloadAsync();
    }

    async Task ReloadAsync()
    {
        await Task.Yield();
        (List<MyNote> items, int total) = MyNoteService.Get(_pageIndex, _pageSize);
        myNotes = items;
        _total = total;
        StateHasChanged();
    }

    private async Task PageIndexChanged(PaginationEventArgs args)
    {
        _pageIndex = args.Page;
        await ReloadAsync();
    }

    private async Task PageSizeChange(PaginationEventArgs args)
    {
        _pageSize = args.PageSize;
        await ReloadAsync();
    }

    public void RemoveSelection(int id)
    {
        var selected = selectedRows.Where(x => x.Id != id);
        selectedRows = selected;
    }

    private async Task DeleteAsync(MyNote myNote)
    {
        MyNoteService.Delete(myNote);
        await ReloadAsync();
    }

    private async Task EditAsync(MyNote myNote)
    {
        CurrentMyNote = myNote.Clone();
        NewRecordMode = false;
        visible = true;
        await Task.Yield();
    }

    private async Task AddAsync()
    {
        CurrentMyNote = new MyNote();
        NewRecordMode = true;
        visible = true;
        await Task.Yield();
    }
    private async Task OnFinishAsync(EditContext editContext)
    {
        if (NewRecordMode == false)
        {
            MyNoteService.Update(CurrentMyNote);
        }
        else
        {
            MyNoteService.Add(CurrentMyNote);
        }
        await ReloadAsync();
        visible = false;
    }

    private void HandleCancel(MouseEventArgs e)
    {
        visible = false;
    }
}

修正 Index.razor 頁面

  • 打開 [Pages] 資料夾下的 [Index.razor] 檔案
  • 使用底下程式碼替換掉這個檔案內容
@page "/"

<h1>Hello, Ant Table CRUD!</h1>

<MyNoteView/>

執行並且測試

  • 按下 F5 開始執行這個專案

  • 網頁出現之後,將會看到底下的畫面

  • 點選任何一筆紀錄右邊的 [Edit] 按鈕,則會看到一個對話窗出現在網頁上

  • 修改 記事名稱 ,接著點選 [儲存] 按鈕,此時,這筆修改的紀錄將會儲存起來

  • 接著,點選 [Delete] 這個按鈕,則會這筆紀錄將會移除了