【ASP.NET Core】標記幫助器——替換元素名稱

2023-02-26 21:00:15

標記幫助器不僅可以給目標元素(標記)插入(或修改)屬性,插入自定義的HTML內容,在某些需求中還可以替換原來標記的名稱。

比如我們在使用 Blazor 時很熟悉的 Component 標記幫助器。在 Razor 檔案中你將使用 <Component> 元素來設定要呈現的元件。而在實際處理時,會去掉 <Component> 元素,並呈現元件的HTML內容。

下面咱們舉兩個例子。

第一個比較簡單,自定義的元素是 <textBox>,生成的元素是 <input type=text />。

[HtmlTargetElement("textBox", TagStructure = TagStructure.WithoutEndTag)]
public class TextInputTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // 將標記名稱改為 input
        output.TagName = "input";
        // 設定 type 屬性
        output.Attributes.SetAttribute("type", "text");
    }
}

這個不難理解,首先我們用的是候,在 Razor 檔案使用的是元素是 <textbox>,而最後呈現出來的 HTML 是 <input> 元素。咱們就把它弄得像個控制元件那樣用,是不是有點以前 ASP.NET 的味兒了?以前是有個字首 asp:TextBox。

TagStructure.WithoutEndTag 的意思是指定這個標記沒有結束標籤,就像 <input > 或 <input />。

Razor 檔案中要用 @addTagHelper 指令匯入一下。

@addTagHelper *, TestApp

TestApp 是我這個程式集的名稱,它匯入此程式集下所有標記幫助器。

用起來也方便。

<div>
    <TextBox />
</div>

執行之後,生成這樣的 HTML。

<input type="text">

咱們確實可以讓它更像傳統 ASP.NET。

@addTagHelper *, TestApp
@tagHelperPrefix "asp:"

<div>
    <asp:textBox />
</div>

@tagHelperPrefix 就是為標記新增一個字首,這裡指定的是「asp:」,所以用的時候就真的撿到「童年回憶」了。想當年,ASP.NET 就是這麼用的。

 

下面再舉一例,咱們做 RadioButton(無線電鈕)。選項之間互斥,同一個分組中你只能選擇一項。在 HTML 中是 <input type=radio >,它沒有「分組」屬性,而是通過 name 屬性來分組。name 相同的就是同一組,互斥。

注意的是,<input type=radio> 只顯示個圈圈讓你可以選中,可不包含選項文字。一般我們會給它配個 <label> 元素。然後,<label> 元素的 for 屬性指向 <input> 的 id 屬性。這樣二者就關聯了,點選<label>也能選中無線電鈕。

所以,咱們實現的幫助器要生成一個<div>,裡面包含<input>和<label>兩個元素。

[HtmlTargetElement("radioButton", TagStructure = TagStructure.WithoutEndTag)]
public class RadioInputTagHelper : TagHelper {

    // 分組名稱
    [HtmlAttributeName("group-name")]
    public string? Group { get; set; } = "radio";

    // id 值
    public string? Id { get; set; } = string.Empty;

    // 標籤文字
    public string? Label { get; set; }

    // 預設是否選中
    public bool IsChecked { get; set; } = false;

    // 選項代表的值
    public string? Value { get; set; } = string.Empty;

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // 修改標記名稱
        output.TagName = null;

        StringBuilder builder = new();
        string html = $"""
        <div>
            <input type="radio" id="{Id}" name="{Group}" value="{Value}" {(IsChecked ? "checked" : "")} />
            <label for="{Id}">{Label}</label>
        </div>
        """;

        // 設定HTML內容
        output.Content.SetHtmlContent(html);
    }
}

預設情況下,在 Razor 檔案中使用標記幫助器時,屬性與.NET類屬性相同。比如這個類中的 Label 屬性,在Razor程式碼中也是Label屬性。

<radioButton Label="老司機" ……>

如果你希望 HTML 屬性與 .NET 屬性不同,可以加上 [HtmlAttributeName] 特性,配分一個別名。

[HtmlAttributeName("group-name")]

咱們這個例子中,就為 Group 屬性分配一個別名—— group-name。

<label> 元素只是呈現在HTML中的文字,讓使用者看,很多時候程式處理的往往不是這個,而是讓標籤對應一個值,即 Value 屬性。Value 屬性指定的值才是給程式用的。

我們來用一下這個無線電鈕。

<div>
    <radioButton group-name="abc" id="itme1" value="1" label="山藥" is-checked="true" />
    <radioButton group-name="abc" id="item2" value="2" label="鴨梨" />
    <radioButton group-name="abc" id="item3" value="3" label="龍眼" />
    <hr />
    <radioButton group-name="good" id="item5" value="5" label="大卡車" />
    <radioButton group-name="good" id="item6" value="6" label="皮卡" is-checked="true" />
    <radioButton group-name="good" id="item4" value="4" label="獨輪車" />
</div>

同一 group-name 下的選項是互斥關係,而不同組之間是互不影響的。

執行後,生成以下 HTML:

 <div>
    <input type="radio" id="itme1" name="abc" value="1" checked />
    <label for="itme1">山藥</label>
</div>
<div>
    <input type="radio" id="item2" name="abc" value="2"  />
    <label for="item2">鴨梨</label>
</div>
<div>
    <input type="radio" id="item3" name="abc" value="3"  />
    <label for="item3">龍眼</label>
</div>
<hr />
<div>
    <input type="radio" id="item5" name="good" value="5"  />
    <label for="item5">大卡車</label>
</div>
<div>
    <input type="radio" id="item6" name="good" value="6" checked />
    <label for="item6">皮卡</label>
</div>
<div>
    <input type="radio" id="item4" name="good" value="4"  />
    <label for="item4">獨輪車</label>
</div>
</div>

結果如下圖所示。

 

接下來看第三個例子。咱們來畫一個圓,標記幫助器定義一個 <circle> 元素,我們直接設定相關屬性就行。比如畫布寬度、畫布高度、圓的圓心、半徑、線的粗細。然後會生成 <canvas> 元素和相關的 js 指令碼程式碼。

[HtmlTargetElement("Circle")]
public class CircleTagHelper : TagHelper
{
    // 圓的半徑
    public int Radius { get; set; }

    // 線寬度
    public int LineWidth { get; set; }

    // 圓心-X
    public int CenterX { get; set; }

    // 圓心-Y
    public int CenterY { get; set; }

    // 畫布寬度
    public int Width { get; set; } = 300;

    // 畫布高度 
    public int Height { get; set; } = 300;

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // 無標記名稱
        output.TagName = null;
        // 生成的內容
        string s = $"""
            <canvas id="cv" width="{Width}" height="{Height}"></canvas>
            <script>
                var canvas = document.getElementById('cv');
                var ctx = canvas.getContext('2d');
                ctx.lineWidth = {LineWidth};
                ctx.strokeStyle = 'blue';
                ctx.beginPath();
                ctx.arc({CenterX}, {CenterY}, {Radius}, 0, 2 * Math.PI);
                ctx.stroke();
            </script>
            """;
        output.Content.SetHtmlContent(s);
    }
}

TagName 設定為 null 表示咱們這個幫助器不生產標籤,而是用輸出字串 s 中那段程式碼。即這段程式碼直接呈現在 HTML 檔案中,外部不用其他元素包裝。

來,咱們在 Razor 檔案中試試看。

<Circle center-x="115" center-y="90"
        width="900"
        height="900"
        line-width="4"
        radius="70" />

繪製的圓如下圖所示。

 

生成的 HTML 如下:

<canvas id="cv" width="900" height="900"></canvas>

<script>
    var canvas = document.getElementById('cv');
    var ctx = canvas.getContext('2d');
    ctx.lineWidth = 4;
    ctx.strokeStyle = 'blue';
    ctx.beginPath();
    ctx.arc(115, 90, 70, 0, 2 * Math.PI);
    ctx.stroke();
</script>

用這個方法,咱們可以封裝出許多圖形繪製的標記,用的時候只要賦值一下屬性即可,很省事的喲。