Blazor入門:ASP.NET Core Razor 元件

痴者工良發表於2020-05-10

官方文件原文位置:
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/components?view=aspnetcore-3.1

本文並不是獨立教程的文章,而是屬於對微軟文件的講解和說明。

元件:專案 Blazor 中,使用 .razor 結尾的檔案,稱為元件;而 Blazor 中的元件,正式名稱是 razor 元件

Blazor 元件是 razor 過渡而來的,使用 razor 的基本語法特性,但是 Balzor 不支援 razor 中的標記幫助程式。

關於元件

.razor 檔案分為頁面(帶@page)和元件(不帶@page,或者說頁面元件和非頁面元件。兩者區別在於頁面有路由,可以直接通過 URI 訪問,一般放在 Page 資料夾中;而元件,作為一個部件,必須嵌入其它元件中,在頁面中顯示,一般放到 Shared 資料夾中,供多個頁面共享、複用。
本文接下來所指的元件都是非頁面元件。
.razor 檔案中,開頭有 @page 標記的,就是頁面元件,沒有的就是非頁面元件。
當然兩者並沒有嚴格的區分。
元件命名時,應該帶上 Component 字尾。

元件類

每個 .razor 檔案,在編譯後會生成一個類,稱為元件類。 生成的類的名稱與檔名匹配。
因此,每個 .razor 檔案,必須以大寫字母開頭,按照類名命名規範定義檔名稱。


`.razor` ,以 `@code{}` 包含 C# 程式碼,這部分程式碼除了元件間可以使用,程式中也可以正常使用,因為屬於類的一部分。

建立 Test.razor 檔案,檔案內容如下:

@code{
    public string Name { get; set; }
}

Pargrom 中:

            Pages.Test test = new Pages.Test();
            test.Name = "Blazor";

簡單來說,就是可以作為一個類來使用。@code{} 中定義的成員,就是類的成員。
成員正常使用 public 、private 等訪問修飾符修飾。

靜態資產

預設靜態資原始檔位置在專案的 wwwroot 目錄,前端(.razor、.cshtml)等,預設定址時,使用絕對路徑 / 即可訪問資源。
例如:

<img alt="Company logo" src="/images/logo.png" />

這個路徑是要放到前端才能,由前端訪問時 ASP.NET Core 框架自動處理,相當於前端訪問 / ,後端訪問 D:/test/Blazor/wwwroot

路由與路由引數

頁面元件使用 @page 設定此頁面的訪問地址,這裡沒有 Controller 和 Action 的分層和路由導航(相對地址),直接是一個絕對的訪問地址,並且全域性唯一。

Index.razor 中,路由:

@page "/"

Blazor 不支援像 Controller 和 Action 那樣設定靈活的 URL 可選引數(URL Query),例如:

        [HttpGet("Test/{Id}")]
        public string Test([FromQuery]int Id)
        {
            return "123";
        }

Blazor 如果想通過 URL Query 傳遞引數,可以使用 {Name}

@page "/test"
@page "/test/{Id}"

<h2>@Id</h2>

@code{
    [Parameter]
    public string Id { get; set; } = "123";
}

因為 Blazor 不支援可選引數,因此,如果只設定 @page "/test/{Id}",那麼每次訪問都必須帶有這個引數值。
需要使用 [Parameter] 來修飾成員,才能捕獲 @page "/test/{Id}"


另外,理由引數是 string 型別,不能自動轉為數值型別。不如會報錯:

InvalidOperationException: Unable to set property 'Id' on object of type 'BlazorApp1.Pages.Test'. The error was: Unable to cast object of type 'System.String' to type 'System.Int32'.

你可以接收後,顯式轉為數值型別。

元件引數

@code 程式碼塊中,使用 [Parameter] 修飾的公共屬性,那麼這個屬性就會標識為元件指定引數。
注意官網文件中,這個小節的程式碼示例,實際是不允許這樣寫得的。
目前,有兩個地方需要使用 [Parameter] 特性,一個是前一小節的路由引數繫結,另一個是嵌入元件時使用。
示例:
Test.razor 檔案內容:

<h2>@Title</h2>

@code{
    [Parameter]
    public string Title { get; set; } = "test";
}

別的元件嵌入 Test.razor 這個元件時,就可以使用 Title 傳遞引數進去:

<Test Title="111" />

請勿建立會寫入其自己的組引數屬性的元件

前面我們說到, [Parameter] 特性的使用,這個特性時作為引數傳遞而使用的。
對於路由引數,其修飾的屬性應該是 privite,對於其它元件傳遞引數,屬性應該設定為 public
如果一個元件的 @code{} 成員不需要被外界作為引數使用,就應該設定為 private
因為 .razor 一般不會作為類來使用。、;而且不設定 [Parameter] 的屬性,別的元件也使用不了這個屬性。

那麼,文件說 “請勿建立會寫入其自己的組引數屬性的元件”,指定是 [Parmeter] 休息的屬性,是作為引數傳遞使用的,不要在元件中修改這個屬性的值。

如果實在要操作的話,可以先拷貝這個值,使用別的變數操作,示例:

<h2>@Title</h2>

@code{
    [Parameter]
    public string Title { get; set; } = "test";

    private string _Title;
    protected override void OnInitialized()
    {
        _Title = Title;
    }
}

這樣,元件要操作的話,可以使用 _Title ,保留 Title
OnInitalized() 是一個元件初始化的方法,也可以理解成建構函式,可以參考 https://docs.microsoft.com/zh-cn/aspnet/core/blazor/lifecycle?view=aspnetcore-3.1#component-initialization-methods

子內容

因為元件是可以巢狀的,可以要求另一個元件顯示要求的內容。

  • 被多個元件使用,不同元件要呈現不一樣的內容;
  • 要根據父元件的配置,顯示子元件;
  • 元件 A 要求使用到的元件 B,顯示其傳遞的內容;

簡單來說,就是將頁面內容作為複雜型別傳遞給另一個元件,要求這個元件顯示出來。
那麼,子內容指的是一個元件可以接收另一個元件的內容,使用 RenderFragment 來接收內容。
示例如下:
Test.razor 中,內容:

<div>@Children</div>

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

另一個元件:

@page "/"

<Test Children=r />
@code{
    private RenderFragment r =@<h1>測試子內容</h1>;
}

RenderFragment 的使用,請自行查閱資料。

屬性展開

屬性展開是使用字典型別表示一個 Html 標籤的多個屬性。

<input id="1"
       maxlength="@Maxlength"
       placeholder="@Placeholder"
       required="@Required"
       size="@Size" />

<input id="2"
       @attributes="InputAttributes" />

@code {
    #region
    private string Maxlength { get; set; } = "10";
    private string Placeholder { get; set; } = "Input placeholder text";
    private string Required { get; set; } = "required";
    private string Size { get; set; } = "50";
    #endregion

    // 使用字典鍵值對錶示
    public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object>()
    {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
     };
}

任意引數

[Paramter] 特性,只有一個屬性,其定義如下:

        public bool CaptureUnmatchedValues { get; set; }

文件說明:[Parameter] 上的 CaptureUnmatchedValues 屬性允許引數匹配所有不匹配任何其他引數的特性。
其作用是通過字典接收在父元件中出現但是未在 @code{} 中定義的引數屬性。
例如:
Test.razor

@code{
    // 這個屬性沒有用,隨便起個名字測試
    [Parameter]
    public string A { get; set; }

    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}

父元件中使用:

<Test A="A"
      B="B"
      C="C" />

B、C 都是 Test.razor 中沒有出現過的,那麼這些引數和引數值都會自動轉為鍵值對儲存到 AdditionalAttributes 中。

測試示例:
Test.razor 中的內容

<ul>
    @foreach (var item in AdditionalAttributes)
    {
        <li>@item.Key - @item.Value</li>
    }
</ul>

@code{
    // 這個屬性沒有用,隨便起個名字測試
    [Parameter]
    public string TTT { get; set; }

    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}

其它元件使用:

@page "/"

<Test TTT="ces"
      id="useIndividualParams"
      maxlength="10"
      placeholder="Input placeholder text"
      required="required"
      size="50" />

捕獲對元件的引用

元件引用提供了一種引用元件例項的方法,使用 @ref 可以實現引用對引數的引用。
建立一個 Test.razor 檔案,內容不限。
在一個元件中,引用該元件例項

@page "/"
<Test @ref="_test" />
@code{
    private Test _test;
}

在使用 Test.razor 元件的同時,保留了引用,以便在 @code{} 中使用其成員。

在外部呼叫元件方法以更新狀態

元件繼承了 ComponentBase 型別,有個 InvokeAsync 方法可用於外界更新此 UI 的狀態。

示例如下:
建立 MyUIServer 型別,

    // 能夠向所有正在開啟的 Index.razor 頁面傳送通知
    public static class MyUIServer
    {
        // 向所有人傳送通知
        public static async Task ToMessage(string message)
        {
            if (events != null)
            {
                await events.Invoke(message);
            }
        }
        public static void AddEvent(Func<string, Task> func)
        {
            events += func;
        }
        public static void RemoveEvent(Func<string, Task> func)
        {
            events -= func;
        }
        private static event Func<string, Task> events;
    }

Index.razor

@page "/"
@using BlazorApp1.Data
@implements IDisposable

<input @bind="_message" />
<button @onclick="Btn">傳送訊息</button>
<ul>
    @foreach (var item in messageList)
    {
        <li>@item</li>
    }
</ul>

@code {
    private string _message;
    private List<string> messageList = new List<string>();
    // 進入頁面時
    protected override void OnInitialized()
    {
        MyUIServer.AddEvent(UIEvent);
    }

    // 退出當前頁面UI後移除該事件
    public void Dispose()
    {
        MyUIServer.RemoveEvent(UIEvent);
    }

    protected async Task UIEvent(string message)
    {
        // 元件自帶的方法,用於外部呼叫更新狀態
        await InvokeAsync(() =>
        {
            messageList.Add(message);
            StateHasChanged();
        });
    }

    // 向所有正在訪問 Index.razor 頁面傳送訊息
    private async Task Btn()
    {
        await MyUIServer.ToMessage(_message);
    }

}

開啟多個視窗,訪問頁面 https://localhost:5001/,在其中一個視窗輸入內容並且點選按鈕,即可將訊息內容推送到其它視窗。

下面是一個修改官網示例的示例:
建立一個型別 NotifierService

    public class NotifierService
    {
        public async Task Update(string key, int value)
        {
            if (Notify != null)
            {
                await Notify.Invoke(key, value);
            }
        }

        public event Func<string, int, Task> Notify;
    }

該型別的 Notify 可以繫結多個事件;通過呼叫 Update() 方法,可以觸發各個事件。
在 Startup 中注入服務 services.AddSingleton<NotifierService>();
Index.razor 中,內容為:

@page "/"
@using BlazorApp1.Data
@inject NotifierService Notifier
@implements IDisposable

<p>Last update: @_lastNotification.key = @_lastNotification.value</p>

@code {
    private (string key, int value) _lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        // 元件自帶的方法,用於外部呼叫更新狀態
        await InvokeAsync(() =>
        {
            _lastNotification = (key, value);
            StateHasChanged();
        });
    }

    // 退出當前頁面UI後移除該事件
    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

Test.razor 檔案中:

@page "/test"
@using BlazorApp1.Data
@inject NotifierService Notifier
Key:
<input @bind="Key" />
Value:
<input @bind="Value" />
<button @onclick="Update">更新</button>

@code{
    private string Key { get; set; }
    private int? Value { get; set; }
    private async Task Update()
    {
        await Notifier.Update(Key, Value.Value);
        Key = string.Empty;
        Value = null;
    }
}

然後啟動專案,一個頁面開啟 https://localhost:5001/ ,另一個頁面開啟 https://localhost:5001/test
test 頁面輸入 Key 和 Value,點選按鈕,即可通知到所有正在開啟 Index.razor 的頁面。

使用 @ 鍵控制是否保留元素和元件

在使用表格或了表等元素時,如果出現插入或刪除、更新等情況,整個表格或列表,就會被重新渲染。這樣會帶來比較大的效能消耗。
一般使用繫結的元素,其更新是自動的,不需要人為控制。
在能保證每一項的某個元素列,都是唯一的時候,我們可以使用 @key 關鍵字來優化元件。
示例:

@page "/"
@using BlazorApp1.Data

Key:
<input @bind="_key" />
Value:
<input @bind="_value" />
<button @onclick="Add">新增</button>
<button @onclick="Remove">移除</button>
<ul>
    @foreach (var item in dic)
    {
        <li @key="item.Key">@item.Key - @item.Value</li>
    }
</ul>

@code {
    private int? _key;
    private int _value;
    private List<MyData> dic { get; set; } = new List<MyData>();
    private void Add()
    {
        if (_key == null)
            return;
        dic.Add(new MyData
        {
            Key = _key.Value,
            Value = _value
        });
        _key = null;
    }
    private void Remove()
    {
        if (_key == null)
            return;
        dic.Remove(dic.First(x => x.Key == _key.Value));
        _key = null;
    }
}

指定基類

@inherits 指令可用於指定元件的基類。 元件都預設繼承了 ComponentBase 。
示例:
建立檔案 TestBase.razor ,內容如下

@code{
    protected int Id { get; set; }
}

建立 Test.razor ,檔案內容如下

@inherits TestBase
@code{ 
    public int Get()
    {
        return Id;
    }
}

指定屬性

可以通過 @attribute 指令在 Razor 元件中指定元件的特性(屬性)。 例如頁面需要登入才能訪問,則新增 [Authorize]

@page "/"
@attribute [Authorize]

匯入元件

當要使用的元件與當前元件在同一個名稱空間時,不需要“匯入”,如果兩者不在同一個名稱空間,則可以使用 @using 匯入此元件。

原始 HTML

使用 MarkupString 型別可以將字串轉為 HTML 元素物件。

@html

@code{ 
    public MarkupString html = (MarkupString)"<h1> Test </h1>";
}

相關文章