共用方式為


使用 Blazor 建置可重複使用的 UI 元件

提示

本內容節錄自《Blazor for ASP NET Web Forms Developers for Azure》電子書,可以從 .NET Docs 取得,也可以免費下載 PDF 離線閱讀。

Blazor-for-ASP-NET-Web-Forms-Developers 電子書封面縮圖。

關於 ASP.NET Web Forms 的其中一項優點,就是如何將可重複使用的使用者介面 (UI) 程式碼片段封裝成可重複使用的 UI 控制項。 您可以使用 .ascx 檔案,在標記中定義自訂使用者控制項。 您也可以使用完整的設計工具支援,在程式碼中建置詳細的伺服器控制項。

Blazor 也支援透過元件進行 UI 封裝。 元件:

  • 這是獨立式 UI 區塊。
  • 維護自身狀態和轉譯邏輯。
  • 可定義 UI 事件處理常式、繫結至輸入資料,以及管理自身生命週期。
  • 通常使用 Razor 語法在 .razor 檔案中定義。

Razor 簡介

Razor 是以 HTML 和 C# 為基礎的輕量型標記範本化語言。 您可以使用 Razor 在標記與 C# 程式碼之間順暢地轉換,以定義您的元件轉譯邏輯。 編譯 .razor 檔案時,會在 .NET 類別中以結構化方式擷取轉譯邏輯。 編譯類別的名稱取自於 .razor 檔案名稱。 命名空間取自於專案的預設命名空間和資料夾路徑,或者您可以使用 @namespace 指示詞明確指定命名空間 (下方提供 Razor 指示詞的詳細資訊)。

元件的轉譯邏輯使用一般 HTML 標記撰寫,並使用 C# 新增動態邏輯。 @ 字元用於轉換至 C#。 在判斷您何時切換回 HTML 時,Razor 通常表現良好。 例如,下列元件會轉譯具有目前時間的 <p> 標記:

<p>@DateTime.Now</p>

若要明確指定 C# 運算式的開頭和結尾,請使用括號:

<p>@(DateTime.Now)</p>

Razor 也可讓您輕鬆地在轉譯邏輯中使用 C# 控制流程。 例如,您可以有條件地轉譯一些 HTML,如下所示:

@if (value % 2 == 0)
{
    <p>The value was even.</p>
}

或者,您也可以使用一般 C# foreach 迴圈產生項目清單,如下所示:

<ul>
@foreach (var item in items)
{
    <li>@item.Text</li>
}
</ul>

Razor 指示詞如同 ASP.NET Web Forms 中的指示詞,可控制 Razor 元件編譯方式的許多層面。 範例包括元件的:

  • Namespace
  • 基底類別
  • 已實作介面
  • 泛型參數
  • 匯入的命名空間
  • 路由

Razor 指示詞以 @ 字元開頭,通常用於檔案開頭的新行開頭。 例如,@namespace 指示詞會定義元件的命名空間:

@namespace MyComponentNamespace

下表摘要列出 Blazor 中所使用的各種 Razor 指示詞及其 ASP.NET Web Forms 對等項目 (若存在)。

指示詞 描述 範例 Web Form 對等項目
@attribute 將類別層級屬性加入元件 @attribute [Authorize]
@code 將類別成員新增至元件 @code { ... } <script runat="server">...</script>
@implements 實作指定介面 @implements IDisposable 使用程式碼後置
@inherits 繼承自指定的基底類別 @inherits MyComponentBase <%@ Control Inherits="MyUserControlBase" %>
@inject 將服務插入元件 @inject IJSRuntime JS
@layout 指定元件的版面配置元件 @layout MainLayout <%@ Page MasterPageFile="~/Site.Master" %>
@namespace 設定元件的命名空間 @namespace MyNamespace
@page 指定元件的路由 @page "/product/{id}" <%@ Page %>
@typeparam 指定元件的泛型型別參數 @typeparam TItem 使用程式碼後置
@using 指定要納入範圍的命名空間 @using MyComponentNamespace web.config 中新增命名空間

Razor 元件也會廣泛使用元素的指示詞屬性,以控制元件編譯方式的各種層面 (事件處理、資料繫結、元件和元素參考等)。 指示詞屬性全都遵循通用泛型語法,其中括號中的值是選擇性的:

@directive(-suffix(:name))(="value")

下表摘要列出 Blazor 中所使用 Razor 指示詞的各種屬性。

屬性 描述 範例
@attributes 轉譯屬性字典 <input @attributes="ExtraAttributes" />
@bind 建立雙向資料繫結 <input @bind="username" @bind:event="oninput" />
@on{event} 為指定事件新增事件處理常式 <button @onclick="IncrementCount">Click me!</button>
@key 指定由差異演算法用於保留集合中元素的索引鍵 <DetailsEditor @key="person" Details="person.Details" />
@ref 擷取元件或 HTML 項目的參考 <MyDialog @ref="myDialog" />

由 Blazor (@onclick@bind@ref 等) 所使用的各種指示詞屬性,會在下列章節和後續章節中討論。

.aspx.ascx 檔案中使用的許多語法在 Razor 中具有平行語法。 下列為 ASP.NET Web Forms 和 Razor 語法的簡單比較。

功能 Web Form 語法 Razor 語法
指示詞 <%@ [directive] %> <%@ Page %> @[directive] @page
程式碼區塊 <% %> <% int x = 123; %> @{ } @{ int x = 123; }
運算式
(HTML 編碼)
<%: %> <%:DateTime.Now %> 隱含:@
明確:@()
@DateTime.Now
@(DateTime.Now)
註解 <%-- --%> <%-- Commented --%> @* *@ @* Commented *@
資料繫結 <%# %> <%# Bind("Name") %> @bind <input @bind="username" />

若要將成員新增至 Razor 元件類別,請使用 @code 指示詞。 這項技術類似於在 ASP.NET Web Forms 使用者控制項或頁面中使用 <script runat="server">...</script> 區塊。

@code {
    int count = 0;

    void IncrementCount()
    {
        count++;
    }
}

由於 Razor 是以 C# 為基礎,因此必須在 C# 專案中編譯 (.csproj)。 您無法從 Visual Basic 專案 (.vbproj) 編譯 .razor 檔案。 您仍然可以從 Blazor 專案參考 Visual Basic 專案。 反之亦然。

如需完整的 Razor 語法參考,請參閱 ASP.NET Core 的 Razor 語法參考

使用元件

除了一般 HTML 之外,元件也可以使用其他元件作為其轉譯邏輯的一部分。 在 Razor 中使用元件的語法,類似於在 ASP.NET Web Forms 應用程式中運用使用者控制項。 元件是使用符合元件類型名稱的元素標記所指定。 例如,您可以新增 Counter 元件,如下所示:

<Counter />

不同於 ASP.NET Web Forms,Blazor 中的元件:

  • 請勿使用元素前置詞 (例如 asp:)。
  • 不需要在頁面或 web.config 中註冊。

請將 Razor 元件視為 .NET 類型,因為兩者性質完全相同。 如果參考包含元件的組件,則該元件可供使用。 若要將元件的命名空間納入範圍,請套用 @using 指示詞:

@using MyComponentLib

<Counter />

正如預設 Blazor 專案中所示,@using 指示詞通常會放入 _Imports.razor 檔案中,以便匯入相同目錄和子目錄中的所有 .razor 檔案中。

如果元件的命名空間不在範圍內,您可以使用其完整類型名稱指定元件,如同在 C# 中一樣:

<MyComponentLib.Counter />

從元件修改頁面標題

在建置 SPA 樣式應用程式時,通常會重新載入頁面的某些部分,而不會重新載入整個頁面。 即使如此,根據目前載入的元件變更頁面標題仍然有用。 這可以透過在元件 Razor 頁面中加入 <PageTitle> 標籤來完成:

@page "/"
<PageTitle>Home</PageTitle>

此元素的內容可以是動態內容,例如顯示目前訊息計數:

<PageTitle>@MessageCount messages</PageTitle>

請注意,如果特定頁面上的數個元件包含 <PageTitle> 標記,則只會顯示最後一個元件 (因為每個元件都會覆寫前一個元件)。

元件參數

在 ASP.NET Web Forms 中,您可以使用公用屬性將參數和資料傳送至控制項。 這些屬性可以使用屬性在標記中設定,或直接在程式碼中設定。 Razor 元件擁有相似的運作方式,雖然元件屬性也必須以 [Parameter] 屬性標示,才能視為元件參數。

下列 Counter 元件定義了名為 IncrementAmount 的元件參數,可用於指定每次按一下按鈕時 Counter 應該遞增的量。

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    int currentCount = 0;

    [Parameter]
    public int IncrementAmount { get; set; } = 1;

    void IncrementCount()
    {
        currentCount+=IncrementAmount;
    }
}

若要在 Blazor 中指定元件參數,請如同您在 ASP.NET Web Forms 中一般使用屬性:

<Counter IncrementAmount="10" />

查詢字串參數

Razor 元件也可以利用它們在其中轉譯的頁面中查詢字串的值作為參數來源。 若要啟用此設定,請將 [SupplyParameterFromQuery] 屬性新增至參數。 例如,下列參數定義會從 ?IncBy=2 格式的要求中取得其值:

[Parameter]
[SupplyParameterFromQuery(Name = "IncBy")]
public int IncrementAmount { get; set; } = 1;

如果您未在 [SupplyParameterFromQuery] 屬性中提供自訂 Name,則根據預設會比對屬性名稱 (在本案例中為 IncrementAmount)。

元件和錯誤界限

根據預設,Blazor 應用程式會偵測未處理的例外狀況,並在頁面底部顯示錯誤訊息,但不會提供其他詳細資料。 若要限制應用程式中受未處理錯誤影響的部分,例如限制對單一元件的影響,可以將 <ErrorBoundary> 標記包裝在元件宣告周圍。

例如,若要防止 Counter 元件擲回可能的例外狀況,請在 <ErrorBoundary> 中加以宣告,並可選擇指定例外狀況發生時要顯示的訊息:

<ErrorBoundary>
    <ChildContent>
        <Counter />
    </ChildContent>
    <ErrorContent>
        Oops! The counter isn't working right now; please try again later.
    </ErrorContent>
</ErrorBoundary>

如果不需要指定自訂錯誤內容,則可以直接包裝元件:

<ErrorBoundary>
  <Counter />
</ErrorBoundary>

如果包裝元件中發生未處理的例外狀況,則會顯示「發生錯誤。」的預設訊息。

事件處理常式

ASP.NET Web Forms 和 Blazor 都提供事件架構程式設計模型來處理 UI 事件。 此類事件的範例包括按鈕點選和文字輸入。 在 ASP.NET Web Forms 中,您可以使用 HTML 伺服器控制項來處理 DOM 所公開的 UI 事件,也可以處理 Web 伺服器控制項所公開的事件。 事件會透過表單回傳要求顯示於伺服器上。 請考慮下列 Web Form 按鈕點選範例:

Counter.ascx

<asp:Button ID="ClickMeButton" runat="server" Text="Click me!" OnClick="ClickMeButton_Click" />

Counter.ascx.cs

public partial class Counter : System.Web.UI.UserControl
{
    protected void ClickMeButton_Click(object sender, EventArgs e)
    {
        Console.WriteLine("The button was clicked!");
    }
}

在 Blazor 中,您可以使用 @on{event} 表單的指示詞屬性,直接註冊 DOM UI 事件的處理常式。 預留位置 {event} 代表事件名稱。 例如,您可以接聽按鈕點選,如下所示:

<button @onclick="OnClick">Click me!</button>

@code {
    void OnClick()
    {
        Console.WriteLine("The button was clicked!");
    }
}

事件處理常式可以接受選擇性的事件特定引數,以提供該事件的詳細資訊。 例如,滑鼠事件可以採用 MouseEventArgs 引數,但並非必要。

<button @onclick="OnClick">Click me!</button>

@code {
    void OnClick(MouseEventArgs e)
    {
        Console.WriteLine($"Mouse clicked at {e.ScreenX}, {e.ScreenY}.");
    }
}

您可以使用 Lambda 運算式,而非參考事件處理常式的方法群組。 Lambda 運算式可讓您關閉其他範圍內的值。

@foreach (var buttonLabel in buttonLabels)
{
    <button @onclick="() => Console.WriteLine($"The {buttonLabel} button was clicked!")">@buttonLabel</button>
}

事件處理常式可以同步或非同步執行。 例如,下列 OnClick 事件處理常式會以非同步方式執行:

<button @onclick="OnClick">Click me!</button>

@code {
    async Task OnClick()
    {
        var result = await Http.GetAsync("api/values");
    }
}

處理事件之後,元件會轉譯為任何元件狀態變更的帳戶。 使用非同步事件處理常式時,元件會在處理常式執行完成後立即轉譯。 元件會在非同步 完成後再次Task轉譯。 這項非同步執行模式可讓您在非同步 Task 仍在進行時轉譯一些適當的 UI。

<button @onclick="ShowMessage">Get message</button>

@if (showMessage)
{
    @if (message == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <p>The message is: @message</p>
    }
}

@code
{
    bool showMessage = false;
    string message;

    public async Task ShowMessage()
    {
        showMessage = true;
        message = await MessageService.GetMessageAsync();
    }
}

元件也可以藉由定義 EventCallback<TValue> 類型的元件參數來定義自己的事件。 事件回呼支援 DOM UI 事件處理常式的所有變化:選擇性引數、同步或非同步、方法群組或 Lambda 運算式。

<button class="btn btn-primary" @onclick="OnClick">Click me!</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClick { get; set; }
}

資料繫結

Blazor 提供簡單的機制,可將資料從 UI 元件繫結至元件的狀態。 此方法與 ASP.NET Web Forms 的功能不同,該功能可用於將資料從資料來源繫結至 UI 控制項。 在處理資料一節中,我們會討論如何處理來自不同資料來源的資料。

若要建立從 UI 元件到元件狀態的雙向資料繫結,請使用 @bind 指示詞屬性。 在下列範例中,核取方塊的值會繫結至 isChecked 欄位。

<input type="checkbox" @bind="isChecked" />

@code {
    bool isChecked;
}

在轉譯元件時,核取方塊的值會設定為 isChecked 欄位的值。 當使用者切換核取方塊時,就會觸發 onchange 事件,並將 isChecked 欄位設定為新值。 此案例中的 @bind 語法相當於下列標記:

<input value="@isChecked" @onchange="(UIChangeEventArgs e) => isChecked = e.Value" />

若要變更繫結所使用的事件,請使用 @bind:event 屬性。

<input @bind="text" @bind:event="oninput" />
<p>@text</p>

@code {
    string text;
}

元件也可以支援資料繫結至其參數。 若要資料繫結,請使用與可繫結參數相同的名稱定義事件回呼參數。 "Changed" 尾碼會新增至名稱。

PasswordBox.razor

Password: <input
    value="@Password"
    @oninput="OnPasswordChanged"
    type="@(showPassword ? "text" : "password")" />

<label><input type="checkbox" @bind="showPassword" />Show password</label>

@code {
    private bool showPassword;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        Password = e.Value.ToString();
        return PasswordChanged.InvokeAsync(Password);
    }
}

若要將資料繫結鏈結至基礎 UI 元素,請直接在 UI 元素上設定值並處理事件,而不是使用 @bind 屬性。

若要繫結至元件參數,請使用 @bind-{Parameter} 屬性來指定您要繫結的參數。

<PasswordBox @bind-Password="password" />

@code {
    string password;
}

狀態變更

如果元件的狀態在一般 UI 事件或事件回呼之外有所變更,則元件必須手動發出需要再次轉譯的訊號。 若要發出元件狀態已變更的信號,請在元件上呼叫 StateHasChanged 方法。

在下列範例中,元件會顯示來自 AppState 服務的訊息,該訊息可由應用程式其他部分更新。 元件會向 AppState.OnChange 事件註冊其 StateHasChanged 方法,以便在訊息更新時轉譯該元件。

public class AppState
{
    public string Message { get; }

    // Lets components receive change notifications
    public event Action OnChange;

    public void UpdateMessage(string message)
    {
        Message = message;
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}
@inject AppState AppState

<p>App message: @AppState.Message</p>

@code {
    protected override void OnInitialized()
    {
        AppState.OnChange += StateHasChanged
    }
}

元件生命週期

ASP.NET Web Forms 架構針對模組、頁面和控制項提供完整定義的生命週期方法。 例如,下列控制項會實作 InitLoadUnLoad 生命週期事件的事件處理常式:

Counter.ascx.cs

public partial class Counter : System.Web.UI.UserControl
{
    protected void Page_Init(object sender, EventArgs e) { ... }
    protected void Page_Load(object sender, EventArgs e) { ... }
    protected void Page_UnLoad(object sender, EventArgs e) { ... }
}

Razor 元件具有完整定義的生命週期。 元件的生命週期可用於初始化元件狀態,並實作進階元件行為。

Blazor 的所有元件生命週期方法都有同步和非同步版本。 元件轉譯為同步的。 您無法在元件轉譯過程中執行非同步邏輯。 所有非同步邏輯都必須在 async 生命週期方法中執行。

OnInitialized

OnInitializedOnInitializedAsync 方法可用於初始化元件。 元件通常會在第一次轉譯後開始初始化。 元件初始化後,在最終處置之前可能還會多次轉譯元件。 OnInitialized 方法類似於 ASP.NET Web Forms 頁面和控制項中的 Page_Load 事件。

protected override void OnInitialized() { ... }
protected override async Task OnInitializedAsync() { await ... }

OnParametersSet

當元件從其父代收到參數,並將值指派給屬性時,會呼叫 OnParametersSetOnParametersSetAsync 方法。 這些方法會在元件初始化後和每次轉譯元件時執行。

protected override void OnParametersSet() { ... }
protected override async Task OnParametersSetAsync() { await ... }

OnAfterRender

OnAfterRenderOnAfterRenderAsync 方法會在元件完成轉譯之後呼叫。 此時會填入元素和元件參考 (下方提供 這些概念的詳細資訊)。 此時會啟用與瀏覽器的互動功能。 可以安全進行與 DOM 和 JavaScript 執行的互動。

protected override void OnAfterRender(bool firstRender)
{
    if (firstRender)
    {
        ...
    }
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await ...
    }
}

在伺服器上預先轉譯時不會呼叫 OnAfterRenderOnAfterRenderAsync

在第一次轉譯元件時 firstRender 參數為 true,否則其值為 false

IDisposable

從 UI 移除元件時,Razor 元件可以實作 IDisposable 來處置資源。 Razor 元件可以使用 @implements 指示詞來實作 IDispose

@using System
@implements IDisposable

...

@code {
    public void Dispose()
    {
        ...
    }
}

擷取元件參考

在 ASP.NET Web Forms 中,通常藉由參考其識別碼,直接在程式碼中操作控制項執行個體。 在 Blazor 中,您也可以擷取及操作元件的參考,儘管此情況不太常見。

若要在 Blazor 中擷取元件參考,請使用 @ref 指示詞屬性。 屬性的值應符合可設定欄位的名稱,且類型應與參考元件相同。

<MyLoginDialog @ref="loginDialog" ... />

@code {
    MyLoginDialog loginDialog = default!;

    void OnSomething()
    {
        loginDialog.Show();
    }
}

父元件轉譯時,欄位會填入子元件執行個體。 然後,您可以呼叫方法或以其他方式操作元件執行個體。

不建議直接使用元件參考來操作元件狀態。 這麼做會阻止元件在正確時間自動轉譯。

擷取元素參考

Razor 元件可以擷取元素的參考。 不同於 ASP.NET Web Forms 中的 HTML 伺服器控制項,您無法使用 Blazor 中的元素參考直接操作 DOM。 Blazor 使用其 DOM 差異演算法為您處理大部分的 DOM 互動。 Blazor 中的擷取元素參考為不透明。 不過,可用於在 JavaScript Interop 呼叫中傳遞特定元素參考。 如需 JavaScript Interop 的詳細資訊,請參閱 ASP.NET Core Blazor JavaScript Interop

樣板化元件

在 ASP.NET Web Forms 中,您可以建立樣板化控制項。 樣板化控制項可讓開發人員指定用於轉譯容器控制項的 HTML 部分。 建置樣板化伺服器控制項的機制很複雜,但可支援以使用者自訂方式轉譯資料的強大案例。 樣板化控制項的範例包括 RepeaterDataList

您也可以藉由定義 RenderFragmentRenderFragment<T> 類型的元件參數,將 Razor 元件樣板化。 RenderFragment 表示可由元件轉譯的 Razor 標記區塊。 RenderFragment<T> 為 Razor 標記區塊,採用可在轉譯片段轉譯時指定的參數。

子內容

Razor 元件可以將其子內容擷取為 RenderFragment,並將該內容轉譯為元件轉譯的一部分。 若要擷取子內容,請定義 RenderFragment 類型的元件參數,並將其命名為 ChildContent

ChildContentComponent.razor

<h1>Component with child content</h1>

<div>@ChildContent</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }
}

然後父元件可以使用一般 Razor 語法提供子內容。

<ChildContentComponent>
    <ChildContent>
        <p>The time is @DateTime.Now</p>
    </ChildContent>
</ChildContentComponent>

範本參數

樣板化 Razor 元件也可以定義 RenderFragmentRenderFragment<T> 類型的多個元件參數。 叫用 RenderFragment<T> 時可以指定其參數。 若要指定元件的泛型型別參數,請使用 @typeparam Razor 指示詞。

SimpleListView.razor

@typeparam TItem

@Heading

<ul>
@foreach (var item in Items)
{
    <li>@ItemTemplate(item)</li>
}
</ul>

@code {
    [Parameter]
    public RenderFragment Heading { get; set; }

    [Parameter]
    public RenderFragment<TItem> ItemTemplate { get; set; }

    [Parameter]
    public IEnumerable<TItem> Items { get; set; }
}

使用樣板化元件時,可以使用符合參數名稱的子項目來指定範本參數。 作為元素傳遞的 RenderFragment<T> 類型元件引數具有名為 context 的隱含參數。 您可以使用子項目上的 Context 屬性,變更此實作參數名稱。 您可以使用符合型別參數名稱的屬性,指定任何泛型型別參數。 如果可能的話,就會推斷型別參數:

<SimpleListView Items="messages" TItem="string">
    <Heading>
        <h1>My list</h1>
    </Heading>
    <ItemTemplate Context="message">
        <p>The message is: @message</p>
    </ItemTemplate>
</SimpleListView>

此元件的輸出如下所示:

<h1>My list</h1>
<ul>
    <li><p>The message is: message1</p></li>
    <li><p>The message is: message2</p></li>
<ul>

程式碼後置

Razor 元件通常會在單一 .razor 檔案中撰寫。 不過,您也可以使用程式碼後置檔案將程式碼和標記分隔。 若要使用元件檔案,請新增符合元件檔名稱但副檔名為 .cs 的 C# 檔案 (Counter.razor.cs)。 使用 C# 檔案來定義元件的基底類別。 您可以任意命名基底類別,不過通常會將類別命名為與元件類別相同的名稱,但新增 Base 副檔名 (CounterBase)。 以元件為基礎的類別也必須衍生自 ComponentBase。 然後,在 Razor 元件檔案中,新增 @inherits 指示詞以指定元件 (@inherits CounterBase) 的基底類別。

Counter.razor

@inherits CounterBase

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button @onclick="IncrementCount">Click me</button>

Counter.razor.cs

public class CounterBase : ComponentBase
{
    protected int currentCount = 0;

    protected void IncrementCount()
    {
        currentCount++;
    }
}

在基底類別中元件成員的可見度必須為 protectedpublic,在元件類別才會是可見的。

其他資源

上述內容並未包含 Razor 元件所有層面的詳盡處理。 如需如何建立和使用 ASP.NET Core Razor 元件的詳細資訊,請參閱 Blazor 文件。