1. 程式人生 > 其它 >Asp.net Core之TagHelper

Asp.net Core之TagHelper

技術標籤:大後端開發

ASP.NET MVC Core的TagHelper(基礎篇)

TagHelper又是一個新的名詞,它替代了自之前MVC版本的HtmlHelper,專注於在cshmlt中輔助生成html標記。

通過使用自定義的TagHelper可以提供自定義的Html屬性或元素,藉助服務端強大的程式設計API,使得cshtml的頁面標記功能更加強大。

利用自定義標記賦予元素功能或新增屬性的方式,跟Angular有點類似。

在MVC Core中內建的很多asp-XXX開頭的TagHelper,後續再介紹,這裡重點看看如何定義自己的TagHelper。

我們通過定義一個簡單的TagHelper來描述他的基本用法:

專案準備

還是基於ASP.NET MVC Core Starter Kit的專案模板建立一個示例專案,具體怎樣用請參考連結中的介紹。

1.這個TagHelper的目的是提供一個設定button樣色的自定義屬性標記,有這個標記的html元素將自動設定對應的css樣式。

<button type="submit" bs-button-color="danger">Add</button>

這裡定義的自定義屬性是bs-button-color,我們預期生成的html是

<button type="submit" class="btn btn-danger">Add</button>

接下來建立TagHelper類,新建一個TagHelpers目錄,新建一個ButtonTagHelper類,如下

複製程式碼

public class ButtonTagHelper : TagHelper
    {
        public string BsButtonColor { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.SetAttribute("class", $"btn btn-{BsButtonColor}");
        }
    }

複製程式碼

這個短小的類,根據程式設計師的思維可以推匯出大概的意思

a.BsButtonColor這個屬性匹配的是bs-button-color這個屬性標記,在執行時會將html屬性轉化成C#物件的屬性

b.然後BsButtonColor的值通過html提供,然後在Process中使用

c.Process方法設定目標元素的class

d.其中TagHelperContext和TagHelperOutput提供了適用的API,拯救了程式設計師,裡面的屬性或者方法夠我們改變世界了。

當然這樣做的話,會讓人產生疑問,那豈不是每個元素都要去匹配一下,沒錯這是預設的行為,後面會提到如何縮小查詢範圍。

對於這樣的功能,如果使用老版本的HtmlHelper實現,那麼會看起來不那麼Html,比如上述的功能用HtmlHelper的實現方式類似如下方式

@Html.TextBoxFor(m => m.Population, new { @class = "form-control" } )

那麼這個寫法對於前端開發人員就不那麼友好了,畢竟別人不同什麼是@Html.TextBoxFor,bs-button-color這種方式更加直接,對html的入侵從視覺上來看更加的友好。

2 註冊TagHelper

光定義還不行,還得註冊讓MVC知道這個自定義的TagHelper。

開啟_ViewImports.cshtml,改成如下內容

@using CustomTagHelper.Models
@addTagHelper CustomTagHelper.TagHelpers.*, CustomTagHelper

其中第二行尤為重要,其目的是說明將我們定義的TagHelper添加註冊到頁面中,這樣頁面就能識match到。

註冊完之後就能呼叫使用了,我們把自定義的TagHelper應用到Home/Create.cshtml的Add按鈕

<button type="submit" bs-button-color="danger">Add</button>

執行之後效果,檢視html可以得知已經生效

image

3 設定TagHelper的應用範圍

剛才我們提到在匹配TagHelper的時候是使用元素型別去匹配,比如我們這裡的ButtonTagHelper,會匹配所有的buttton。但實際上這不是我們需要的,我們只希望出現了自定義html標記屬性的元素才應用這個TagHelper。

MVC Core為我們提供了HtmlTargetElement屬性標記類解決這個問題,讓TagHelper的應用更加的精準。

我們更新ButtonTagHelper,加入HtmlTargetElement屬性

複製程式碼

[HtmlTargetElement("button", Attributes = "bs-button-color", ParentTag = "form")]
    public class ButtonTagHelper : TagHelper
    {
        public string BsButtonColor { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.SetAttribute("class", $"btn btn-{BsButtonColor}");
        }
    }

複製程式碼

看看HtmlTargetElement的定義,也是一目瞭然

1 第一個引數是tag型別

2 Attributes定義用於選擇匹配應用元素的屬性

3 ParentTag,設定必須是某個html的子元素才設定

當然有了HtmlTargetElement做護航,我們除了可以縮小範圍,當然也可以用它來擴大影響範圍。

比如我們把第一個tag引數去掉,那麼就是應用到所有的元素型別(當然也要滿足Attributes和ParentTag條件)

然後把tag引數去掉之後,發現範圍太大不好管控,萬一使用者不知道這範圍定義,那麼就會導致樣式錯誤,並且這種bug不好跟,一旦發生也是個坑。

所以既要支援多種tag,又不能汙染太多,那麼就再apply一個HtmlTargetElement屬性,比如如下

複製程式碼

[HtmlTargetElement("button", Attributes = "bs-button-color", ParentTag = "form")]

[HtmlTargetElement("a", Attributes = "bs-button-color", ParentTag = "form")]
    public class ButtonTagHelper : TagHelper
    {
        public string BsButtonColor { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.SetAttribute("class", $"btn btn-{BsButtonColor}");
        }
    }

複製程式碼

這裡用了兩個HtmlTargetElement,指明瞭只有button和a元素可以應用這個tagHelper,後續哪個人複製貼上到了其他tag也不會受影響。

示例程式碼

https://github.com/shenba2014/AspDotNetCoreMvcExamples/tree/master/CustomTagHelper

先寫到這,下一篇將介紹一些稍微高階一點用法。

ASP.NET MVC Core的TagHelper (高階特性)

這篇博文ASP.NET MVC Core的TagHelper(基礎篇)介紹了TagHelper的基本概念和建立自定義TagHelper的方式,接著繼續介紹一些新的看起來比較高階的特性。(示例程式碼緊接著上一遍博文)

一、使用自定義的標記元素

之前基礎篇介紹的TagHelper的功能是給已有的HTML元素提供一個自定義的屬性標記,然後伺服器認出這個標記後,將標記轉化成最終的HTML。這裡將要介紹的功能是,定義個全新的Tag,看起來跟普通的HTML元素一樣。是不是覺得很熟悉呢(前提是你用過AngularJS),完全類似於AngularJS的強大的元素定義功能。

比如我們這裡建立一個新的標記元素,formbutton,使用方式如下

<formbutton type="submit" bg-color="danger" />

當然這個標記完全不是HTML內部定義的,瀏覽器也不能認出這是個啥玩意。

這個Tag跟自定義的屬性標記一樣,都會被MVC Core框架識別出來,然後轉化成最終的HTML。

接下來我們建立這個TagHelper

在TagHelpers資料夾新建一個類

複製程式碼

[HtmlTargetElement("formbutton")]
    public class FormButtonTagHelper : TagHelper
    {
        public string Type { get; set; } = "Submit";

        public string BgColor { get; set; } = "primary";

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "button";
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Attributes.SetAttribute("class", $"btn btn-{BgColor}");
            output.Attributes.SetAttribute("type", Type);
            output.Content.SetContent(Type == "submit" ? "Add" : "Reset");
        }
    }

複製程式碼

這個class定義的兩個屬性Type和BgColor,如大部分的猜想,這兩個屬性會匹配成html中定義的屬性,然後把值自動賦給TagHelper Instance中的屬性。

Process一連串的output呼叫也比較直接,大概意思是要生成一個button元素,並且根據使用者提供的Type和BgColor生成class和type兩個html屬性的值。

其中SetContent是要設定需要輸出的內容,由於TagMode是StartTagAndEndTag,所以內容會顯示在標記之間。

接下來在home/create這個頁面使用我們的自定義標記

@model City
@{ Layout = "_Layout"; }
<form method="post" action="/Home/Create">
<div class="form-group">
<label for="Name">Name:</label>
<input class="form-control" name="Name" />
</div>
<div class="form-group">
<label for="Country">Country:</label>
<input class="form-control" name="Country" />
</div>
<div class="form-group">
<label for="Population">Population:</label>
<input class="form-control" name="Population" />
</div>
<formbutton type="submit" bg-color="danger" />
<formbutton type="reset" />
<a bs-button-color="primary" href="/Home/Index">Cancel</a>
</form>

我們使用formbuttion分別建立了一個submit和reset按鈕,並且給submit按鈕設定了danger樣式

那麼這兩個按鈕輸出後的html分別是

<button class="btn btn-danger" type="submit">Add</button>

<button class="btn btn-primary" type="reset">Reset</button>

這是建立一個基本的自定義TagHelper的使用方式。

二、在目標元素之前或者之後插入內容

上一個栗子,比較中規中矩,實際上我們經常需要給元素前後插入一些內容,通常是一些外圍包含元素。比如有如下元素

<div title="Cities"></div>

我們希望這個標記在輸出成html的時候能在前後都自動加上一個div class=panel-body的色塊,那麼我麼可以利用TagHelperOutput提供的方法實現。

可以建立如下的自定義TagHelper來說明

在TagHelpers資料夾新建類ContentWrapperTagHelper

複製程式碼

[HtmlTargetElement("div", Attributes = "title")]
    public class ContentWrapperTagHelper : TagHelper
    {
        public bool IncludeHeader { get; set; } = true;
        public bool IncludeFooter { get; set; } = true;

        public string Title { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.SetAttribute("class", "panel-body");

            var title = new TagBuilder("h1");
            title.InnerHtml.Append(Title);

            var container = new TagBuilder("div");
            container.Attributes["class"] = "bg-info panel-body";
            container.InnerHtml.AppendHtml(title);


            if (IncludeHeader)
            {
                output.PreElement.SetHtmlContent(container);
            }

            if (IncludeFooter)
            {
                output.PostElement.SetHtmlContent(container);
            }
        }
    }

複製程式碼

1.這裡指定了TagHelper的應用範圍是包含了title屬性的div元素

2.分別提供了IncludeHeader和IncludeFooter的屬性,預設都是true

3.然後分別使用PreElement和PostElement設定前後內容

我們把這個標籤應用在_Layout檔案中

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Cities</title>
<link href="/lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="panel-body">
<div title="Cities">@RenderBody()</div>
</body>
</html>

執行就能看到頭部和底部分別都輸出了一個色塊,並且包含了標題內容

三、在已有標記內容中插入內容

上一個栗子講的是插入元素,這裡演示一下插入內容到標籤中,比如已有標籤裡面已經有內容了,可以在內容之前或者之後插入內容。

在TagHelpers目錄新建一個TableCellTagHelper類

複製程式碼

[HtmlTargetElement("td", Attributes = "wrap")]
    public class TableCellTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.PreContent.SetHtmlContent("<b><i>");
            output.PostContent.SetHtmlContent("</i></b>");
        }
    }

複製程式碼

通過使用TagHelperOutput的PreContent和PostContent,分別在已有內容的前後插入了一段html標記包裹,這個TagHelper只會用於帶有wrap屬性的td標記。

把這個標記用在Home/Index.cshtml頁面,把city的名稱的td加入wrap屬性即可

@model IEnumerable<City>
@{ Layout = "_Layout"; }
<table class="table table-condensed table-bordered">
<thead class="bg-primary">
<tr>
<th>Name</th>
<th>Country</th>
<th class="text-right">Population</th>
</tr>
</thead>
<tbody>
@foreach (var city in Model)
{
<tr>
<td wrap>@city.Name</td>
<td>@city.Country</td>
<td class="text-right">@city.Population?.ToString("#,###")</td>
</tr>
}
</tbody>
</table>
<a href="/Home/Create" class="btn btn-primary">Create</a>

執行後可以看到已有的內容都被<i><b></b></i>包裹起來,呈現的是加粗和斜體的效果。

四、使用ViewModel提供的屬性值

在VIew裡面輸出ViewModel的值,經常會用到一些強型別的幫助方法,比如asp-for="Name"等,那麼實際上就會讀取ViewModel的Name的屬性值。

自定義的TagHelper也支援這種方式,我們來看一下如何呼叫,還是繼續在TagHelpers目錄新建一個類,如下

LabelAndInputTagHelper

複製程式碼

[HtmlTargetElement("label", Attributes = "helper-for")]
    [HtmlTargetElement("input", Attributes = "helper-for")]
    public class LabelAndInputTagHelper : TagHelper
    {
        public ModelExpression HelperFor { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (output.TagName == "label")
            {
                output.TagMode = TagMode.StartTagAndEndTag;
                output.Content.Append(HelperFor.Name);
                output.Attributes.SetAttribute("for", HelperFor.Name);
            } else if (output.TagName == "input")
            {
                output.TagMode = TagMode.SelfClosing;
                output.Attributes.SetAttribute("name", HelperFor.Name);
                output.Attributes.SetAttribute("class", "form-control");
                if (HelperFor.Metadata.ModelType == typeof(int?))
                {
                    output.Attributes.SetAttribute("type", "number");
                }
            }
        }
    }

複製程式碼

這個TagHelper的作用用,將for屬性應用到label和input元素上,實現常見的點選label後聚焦到input的功能。

這裡一個關鍵屬性是HelperFor,用來讀取ViewModel提供的屬性的資訊,它的型別是ModelExpression,看起來比較高階,用它可以很方便得到ViewModel的資訊。

我們把這個TagHelper應用到Home/Create.cshtml頁面中

比如之前我們是這樣寫的

<label for="Name">Name:</label>

<input class="form-control" name="Name" />

現在用了標記之後就可以簡化成如下

<label helper-for="Name"/>

<input helper-for="Name"/>

看起來更加的整潔,和符合強迫症程式設計師的口味。

五. TagHelper之前相互通訊協同

兩個不同的TagHelper之前實際上可以通過共享資料的方式實現協同,當然共享資料的方式很多啊,比如粗暴一點的用資料,什麼Session之類的(經常面試被問到的Asp.net頁面傳遞有哪些方法啊,通常是老傢伙裝13的樣子在問)

當然我們不會用資料或者Session去儲存共享的資料,TagHelperContext為我們提供了一個便利的實現方式,類似於HttpContent.Items,直接看看例子。

在TagHelpers資料夾新建一個CoordinatingTagHelpers檔案

複製程式碼

[HtmlTargetElement("div", Attributes = "theme")]
    public class ButtonGroupThemeTagHelper : TagHelper
    {
        public string Theme { get; set; }

        public override void Process(TagHelperContext context,
            TagHelperOutput output)
        {
            context.Items["theme"] = Theme;
        }
    }

    [HtmlTargetElement("button", ParentTag = "div")]
    [HtmlTargetElement("a", ParentTag = "div")]
    public class ButtonThemeTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context,
            TagHelperOutput output)
        {
            if (context.Items.ContainsKey("theme"))
                output.Attributes.SetAttribute("class",
                    $"btn btn-{context.Items["theme"]}");
        }
    }

複製程式碼

這個檔案包含兩個TagHelper,第一個是定義了div標籤,它在context.Items裡設定了theme的值,然後在另外一個TagHelper中讀取items的值。

用法簡單到沒有朋友

<div theme="primary">
<button type="submit">Add</button>
<button type="reset">Reset</button>
<a href="/Home/Index">Cancel</a>
</div>

裡面的button的樣式會根據外層theme的值來設定對應的樣式,比如設定theme="Danger",裡面的按鈕顯示為如下樣式

image

六. 禁止內容輸出

最後要介紹的是禁止內容輸出。禁止內容輸出很多方法,最簡單的不顯示或者加個if else判斷。

這裡使用TagHelperOutput提供的SuppressOutput方法。

新建如下TagHelper

複製程式碼

[HtmlTargetElement(Attributes = "show-for-action")]
    public class SelectiveTagHelper : TagHelper
    {
        public string ShowForAction { get; set; }
        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }
        public override void Process(TagHelperContext context,
        TagHelperOutput output)
        {
            if (!ViewContext.RouteData.Values["action"].ToString()
            .Equals(ShowForAction, StringComparison.OrdinalIgnoreCase))
            {
                output.SuppressOutput();
            }
        }
    }

複製程式碼

這個TagHelper定義了其標籤內容只有在當前Action跟目標Action一致的時候在顯示內容,否則呼叫Suppress禁止內容輸出

比如如下html標記

<div show-for-action="Index" class="panel-body bg-danger">
<h2>Important Message</h2>
</div>

指定了只有在Index action下才顯示important Message

示例程式碼路徑

https://github.com/shenba2014/AspDotNetCoreMvcExamples/tree/master/CustomTagHelper