探讨TagHelper的實作
衆所周知,在asp.net core中編寫Razor視圖的時候,用了一種新的寫法--TagHelper
那這個TagHelper是怎麼回事呢?
首先來看看TagHelper的項目位置,它是位于Microsoft.AspNetCore.Mvc.TagHelpers。
如果看到project.json,可以發現,它還依賴一個比較重要的東西Microsoft.AspNetCore.Mvc.Razor
為什麼這麼說呢,其實很簡單,看了裡面諸多TagHelper,就會發現,裡面都是繼承了
Microsoft.AspNetCore.Razor.TagHelpers下面的TagHelper這個抽象類。
下面就以我們天天用到的表單--FormTagHelper為例來說一下,他是怎麼實作的。
首先要看看TagHelper這個抽象類:
1 public abstract class TagHelper : ITagHelper
2 {
3 protected TagHelper();
4 public virtual int Order { get; }
5 public virtual void Init(TagHelperContext context);
6 public virtual void Process(TagHelperContext context, TagHelperOutput output);
7 public virtual Task ProcessAsync(TagHelperContext context, TagHelperOutput output);
8 }
裡面包含兩比較重要的方法:Process和ProcessAsync
其實看方法名就應該知道一個是同步的方法一個是異步的方法
因為這個是輸出html的方法,你說,這能不重要嗎?下面來看看FormTagHelper的具體實作吧!
1 [HtmlTargetElement("form", Attributes = ActionAttributeName)]
先來看看HtmlTargetElement這個Attribute是用來幹嘛的
簡單來說,它指定了我們html标簽(<form></form>)以及一些相關的元素。
可以看到,諸多Attributes = XXXAttributeName,其中的XXXAttributeName是在類裡面定義的變量。
1 private const string ActionAttributeName = "asp-action";
2 private const string AntiforgeryAttributeName = "asp-antiforgery";
3 private const string AreaAttributeName = "asp-area";
4 private const string ControllerAttributeName = "asp-controller";
5 private const string RouteAttributeName = "asp-route";
6 private const string RouteValuesDictionaryName = "asp-all-route-data";
7 private const string RouteValuesPrefix = "asp-route-";
8 private const string HtmlActionAttributeName = "action";
再來看看下面的圖,相對比一看,是不是就很清晰了呢?

我們可以看到下面的好幾個屬性,如Controller,它的上面是有 HtmlAttributeName來标注的
而且這個指向的名字還是ControllerAttributeName(也就是asp-controller)。這個就是用來接收asp-controller的值。
1 [HtmlAttributeName(ControllerAttributeName)]
2 public string Controller { get; set; }
相對來說,這樣做隻是起了個别名。
1 [HtmlTargetElement("form", Attributes = ActionAttributeName)]
2 [HtmlTargetElement("form", Attributes = AntiforgeryAttributeName)]
3 [HtmlTargetElement("form", Attributes = AreaAttributeName)]
4 [HtmlTargetElement("form", Attributes = ControllerAttributeName)]
5 [HtmlTargetElement("form", Attributes = RouteAttributeName)]
6 [HtmlTargetElement("form", Attributes = RouteValuesDictionaryName)]
7 [HtmlTargetElement("form", Attributes = RouteValuesPrefix + "*")]
8 public class FormTagHelper : TagHelper
當然,我們也是可以不指定别名的,也可以不用在HtmlTargetElement指明Attributes
好比如下的代碼,就可以直接用Controller
1 [HtmlTargetElement("form")]
2 public class FormTagHelper : TagHelper
3 {
4 public string Controller { get; set; }
5 }
還有一個RouteValues的屬性,它是一個鍵值對,用來存放參數的,具體可以怎麼用呢?
總的來說有兩種用法。可以看到它指向asp-all-route-data和asp-route-
1 [HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)]
用法如下:一種是用asp-all-route-data來接收一個IDictionary類型的變量,一種是通過asp-route-*的方式來接收參數*的值。
這兩種寫法是等價的。
下面就是FormTagHelper的構造函數和一個Generator屬性
1 public FormTagHelper(IHtmlGenerator generator)
2 {
3 Generator = generator;
4 }
5 protected IHtmlGenerator Generator { get; }
由于在Core中,依賴注入随處可見,看到這個寫法馬上就是想到了這個
果不其然,發現其對應了一個實作類:DefaultHtmlGenerator。
1 public class DefaultHtmlGenerator : IHtmlGenerator
2 {
3 public DefaultHtmlGenerator(IAntiforgery antiforgery, IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, ClientValidatorCache clientValidatorCache);
4 public virtual TagBuilder GenerateActionLink(ViewContext viewContext, string linkText, string actionName, string controllerName, string protocol, string hostname, string fragment, object routeValues, object htmlAttributes);
5 public virtual IHtmlContent GenerateAntiforgery(ViewContext viewContext);
6 public virtual TagBuilder GenerateForm(ViewContext viewContext, string actionName, string controllerName, object routeValues, string method, object htmlAttributes);
7 public virtual TagBuilder GenerateLabel(ViewContext viewContext, ModelExplorer modelExplorer, string expression, string labelText, object htmlAttributes);
8 public virtual TagBuilder GenerateTextArea(ViewContext viewContext, ModelExplorer modelExplorer, string expression, int rows, int columns, object htmlAttributes);
9 public virtual TagBuilder GenerateTextBox(ViewContext viewContext, ModelExplorer modelExplorer, string expression, object value, string format, object htmlAttributes);
10 protected virtual TagBuilder GenerateInput(ViewContext viewContext, InputType inputType, ModelExplorer modelExplorer, string expression, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes);
11 protected virtual TagBuilder GenerateLink(string linkText, string url, object htmlAttributes);
12 ....省略部分
13 }
這個類裡面,我們看到了熟悉的TagBuilder,就算不去看它裡面的實作都能知道它是用來幹嘛的
它就是用來建立我們的Html标簽,相信用過MVC的,多多少少都擴充過HtmlHelper,這是類似的。
最後,也是最最重要的重寫的Process方法。
可以看到開始就判斷了表單<form>中是否包含了action這個屬性output.Attributes.ContainsName(HtmlActionAttributeName)
如果包含,就是正常的html标簽。換句話說,正常的html寫法和我們的TagHelper方法會有沖突,隻能用其中一種。
當我們這樣寫的時候,編譯能通過。
但是,運作的時候就會出錯。
再下面的處理就是用了TagBuilder去處理了。
收集路由的資料放到一個字典中->區域是否存在->用Generator去建立form表單,傳回TagBuilder對象->TagHelperOutput對象把tagbuilder的innerhtml等資訊輸出。
如下面的寫法:
1 <form method="post" asp-action="Get" asp-controller="Product" asp-antiforgery="false" asp-route-id="2">
2 <button type="submit">submit</button>
3 </form>
生成對應的html如下:
1 <form method="post" action="/Product/Get/2">
2 <button type="submit">submit</button>
3 </form>
到這裡,FormTagHelper的講解就算是OK,至于其他的,原理都是差不多,就不再累贅了。
來看看,到底有多少種TagHelper(還沒有部分沒有列出來),以及它們包含的屬性。
下面是我們自己寫一個TagHelper——CatcherATagHelper,這個TagHelper是幹什麼的呢?它隻是一個精簡版的A标簽。
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.AspNetCore.Mvc.Rendering;
3 using Microsoft.AspNetCore.Mvc.Routing;
4 using Microsoft.AspNetCore.Mvc.TagHelpers;
5 using Microsoft.AspNetCore.Mvc.ViewFeatures;
6 using Microsoft.AspNetCore.Razor.TagHelpers;
7
8 namespace Catcher.EasyDemo.Controllers.TagHelpers
9 {
10 [HtmlTargetElement("catcher-a")]
11 public class CatcherATagHelper:TagHelper
12 {
13 public CatcherATagHelper(IHtmlGenerator generator, IUrlHelperFactory urlHelperFactory)
14 {
15 this.Generator = generator;
16 UrlHelperFactory = urlHelperFactory;
17 }
18
19 [HtmlAttributeNotBound]
20 public IUrlHelperFactory UrlHelperFactory { get; }
21
22 protected IHtmlGenerator Generator { get; }
23
24 public override int Order
25 {
26 get
27 {
28 return -1000;
29 }
30 }
31
32 public string Action { get; set; }
33
34 public string Controller { get; set; }
35
36 public string LinkText { get; set; }
37
38 [ViewContext]
39 [HtmlAttributeNotBound]
40 public ViewContext ViewContext { get; set; }
41
42 public override void Process(TagHelperContext context, TagHelperOutput output)
43 {
44 //method 1
45 if (Action != null || Controller != null)
46 {
47 output.Attributes.Clear();
48
49 var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
50
51 output.TagName = "a";
52
53 output.Attributes.SetAttribute("href", urlHelper.Action(Action, Controller));
54 //whether the inner html is null
55 if (output.Content.IsEmptyOrWhiteSpace)
56 {
57 output.PreContent.SetContent(LinkText);
58 }
59 }
60 //method 2
61 //TagBuilder tagBuilder;
62 //if (Action != null || Controller != null)
63 //{
64 // tagBuilder = Generator.GenerateActionLink(
65 // ViewContext,
66 // linkText: string.Empty,
67 // actionName: Action,
68 // controllerName: Controller,
69 // protocol: string.Empty,
70 // hostname: string.Empty,
71 // fragment: string.Empty,
72 // routeValues: null,
73 // htmlAttributes: null);
74
75 // output.TagName = "a";
76 // //whether the inner html is null
77 // if (output.Content.IsEmptyOrWhiteSpace)
78 // {
79 // output.PreContent.SetContent(LinkText);
80 // }
81 // output.MergeAttributes(tagBuilder);
82 //}
83 }
84 }
85 }
這裡提供了兩種寫法供大家參考
一種是借助IUrlHelperFactory去生成連結
一種是借助IHtmlGenerator去生成連結
寫好了之後要怎麼用呢?
不知道大家有沒有留意_ViewImports.cshtml這個檔案
1 @using Catcher.EasyDemo.Website
2 @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
3 @inject Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration TelemetryConfiguration
這個是預設情況下幫我們添加的TagHelper
我們可以在要用到那個TagHelper的地方添加就好
1 @{
2 Layout = null;
3 }
4 @addTagHelper Catcher.EasyDemo.Controllers.TagHelpers.CatcherATagHelper , Catcher.EasyDemo.Controllers
5 <catcher-a action="list" controller="product" link-text="text">With LinkText And InnerHtml</catcher-a>
6 <br />
7 <catcher-a action="list" controller="product" link-text="">Without LinkText</catcher-a>
8 <br />
9 <catcher-a action="list" controller="product" link-text="Only With LinkText"></catcher-a>
Index.cshtml
addTagHelper的用法如下:
@addTagHelper 你的TagHelper , 你的TagHelper所在的命名空間
或者更直接
@addTagHelper * , 你的TagHelper所在的命名空間
可以添加,當然也可以删除,删除是@removeTagHelper
當我們在自己的架構中完全重寫了一套自己的TagHelper,那麼這個時候,微軟自己的TagHelper我們就可以通過下面的方法來移除了。
@removeTagHelper * , Microsoft.AspNetCore.Mvc.TagHelpers
如果您認為這篇文章還不錯或者有所收獲,可以點選右下角的【推薦】按鈕,因為你的支援是我繼續寫作,分享的最大動力!
作者:Catcher Wong ( 黃文清 )
來源:http://catcher1994.cnblogs.com/
聲明:
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。如果您發現部落格中出現了錯誤,或者有更好的建議、想法,請及時與我聯系!!如果想找我私下交流,可以私信或者加我微信。