這篇博文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",里面的按鈕顯示為如下樣式
六. 禁止內容輸出
最后要介紹的是禁止內容輸出。禁止內容輸出很多方法,最簡單的不顯示或者加個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