9.2.3 .net core 通過TagHelper封裝控件


.net core 除了繼續保留.net framework的HtmlHelper的寫法以外,還提供了TagHelper和ViewComponent方式生成控件。

我們本節說的是使用TagHelper來生成控件。不過嚴格的說起來,TagHelper是對客戶端html元素的輔助類,例如渲染、增加服務端特性等。我們可以使用 taghelper 定義自己的標簽或更改已知標簽。使用TagHelper,VS.net編譯環境也可以自動感知,並提供智能提示。因為TagHelper生成的控件,看起來像一個原生的HTML標簽一樣,也更利於美工進行頁面美化。

例如一個lable控件 <label asp-for="Email"></label>生成的最終html就是這樣:<label for="Email">xxx@xx.com</label>

 

如果要使用TagHelper,除了在頁面中using 命名空間之外,還需要使用@addTagHelper來使TagHelper可用。由於我們會編寫不止一個TagHelper,且在多個cshtml頁面使用,因此我們將如下代碼

@using MicroStrutLibrary.Presentation.Web.Controls

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@addTagHelper *, MicroStrutLibrary.Presentation.Web.Controls

寫在視圖文件 Views/_ViewImports.cshtml中。寫在Views/_ViewImports.cshtml的意思是,默認所有的在 Views 和Views下級目錄中的視圖文件都可以使用TagHelper,通配符 ("*") 來指定在特定程序集(Microsoft.AspNetCore.Mvc.TagHelpers和MicroStrutLibrary.Presentation.Web.Controls)中的所有的 TagHelpers 在 Views目錄和子目錄中的視圖文件中都是可用的。第一個程序集是mvc自帶的,第二個是我們自己的控件程序集。具體的說明大家還是看.net core文檔吧。

 

下面,我們仍舊以上一節介紹的多選控件MultiSelect為例,說明下我們的TagHelper封裝過程。

MultiSelect在cshtml中的寫法如下,asp-dataSource傳入的是數據源,select的所有下拉內容都是通過datasource生成的,而asp-value傳入的是選中的項:

1 @model Dictionary<string, string>
2 @{ 
3     List<string> values = ViewData.Get<List<string>>("Values");
4 }
5 
6 <multiSelect id="txtInput0" asp-dataSource="@Model" asp-value="@values"></multiSelect> 

 

Model和values從controller中傳過來的,具體如下:

 1         public IActionResult MultiSelect()
 2         {
 3             Dictionary<string, string> data = new Dictionary<string, string>();
 4             data.Add("1", "Aaaaa");
 5             data.Add("2", "Aaron");
 6             data.Add("3", "Abbott");
 7             data.Add("4", "Abel");
 8             data.Add("5", "Abner");
 9             data.Add("6", "Abraham");
10             data.Add("7", "Adair");
11             data.Add("8", "Adam");
12             data.Add("9", "Addison");
13 
14             List<string> value = new List<string>();
15             value.Add("2");
16             value.Add("7");
17             ViewData["Values"] = value;
18 
19             return View(data);
20         }

 

這樣,最終生成的html代碼和頁面如下:

 1 <div>
 2   <select id="txtInput0" name="txtInput0" multiple="multiple">
 3     <option value="1" >Aaaaa</option>
 4     <option value="2" selected>Aaron</option>
 5     <option value="3" >Abbott</option>
 6     <option value="4" >Abel</option>
 7     <option value="5" >Abner</option>
 8     <option value="6" >Abraham</option>
 9     <option value="7" selected>Adair</option>
10     <option value="8" >Adam</option>
11     <option value="9" >Addison</option>
12   </select>
13   <script>
14     require(['jquery', 'bootstrap', 'multiselect'], function ($) {
15       $(function(){
16         $("#txtInput0").multiselect({
17             nonSelectedText: '請選擇',
18             enableFiltering: true,//是否顯示過濾
19             filterPlaceholder: '查找',
20             includeSelectAllOption: true,//是否顯示全選
21             selectAllText: '全選',
22             numberDisplayed: 5//顯示條數
23         });
24           });
25     });
26   </script>
27 </div> 

 

MultiSelect在頁面展示如下圖

按照設計,MultiSelect應該能夠傳入:1、數據源,也就是下拉列表的所有數據;2、已選項,已選擇的數據;3、下拉列表的展示條數,以免條數過多,影響頁面;4、是否只讀等屬性,更好的控制控件的展示內容。當然了,還應該包括一個For屬性,這個For屬性的意思是通過頁面的Model綁定,自動生成控件Id和已選擇數據項等內容。

由於所有的TagHelper都繼承於一個基類TagHelper,我們首先看下TagHelper抽象類:

 1     //
 2     // 摘要:
 3     //     Class used to filter matching HTML elements.
 4     public abstract class TagHelper : ITagHelper
 5     {
 6         protected TagHelper();
 7 
 8         //
 9         // 備注:
10         //     Default order is 0.
11         public virtual int Order { get; }
12 
13         //
14         public virtual void Init(TagHelperContext context);
15         //
16         // 摘要:
17         //     Synchronously executes the Microsoft.AspNetCore.Razor.TagHelpers.TagHelper with
18         //     the given context and output.
19         //
20         // 參數:
21         //   context:
22         //     Contains information associated with the current HTML tag.
23         //
24         //   output:
25         //     A stateful HTML element used to generate an HTML tag.
26         public virtual void Process(TagHelperContext context, TagHelperOutput output);
27         //
28         // 摘要:
29         //     Asynchronously executes the Microsoft.AspNetCore.Razor.TagHelpers.TagHelper with
30         //     the given context and output.
31         //
32         // 參數:
33         //   context:
34         //     Contains information associated with the current HTML tag.
35         //
36         //   output:
37         //     A stateful HTML element used to generate an HTML tag.
38         //
39         // 返回結果:
40         //     A System.Threading.Tasks.Task that on completion updates the output.
41         //
42         // 備注:
43         //     By default this calls into Microsoft.AspNetCore.Razor.TagHelpers.TagHelper.Process(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext,Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput).
44         public virtual Task ProcessAsync(TagHelperContext context, TagHelperOutput output);
45     }

 

這里最重要的就是Process方法和ProcessAsync方法。這兩個方法,一個是同步,一個是異步,作用都是輸出頁面html代碼。

我們再來看封裝后的MultiSelectTagHelper的代碼:

  1     [HtmlTargetElement("multiSelect", Attributes = ForAttributeName)]
  2     [HtmlTargetElement("multiSelect", Attributes = ValueAttributeName)]
  3     [HtmlTargetElement("multiSelect", Attributes = ShowItemCountAttributeName)]
  4     [HtmlTargetElement("multiSelect", Attributes = DataSourceAttributeName)]
  5     [HtmlTargetElement("multiSelect", Attributes = ReadonlyAttributeName)]
  6     public class MultiSelectTagHelper : TagHelper
  7     {
  8         private readonly IHtmlGenerator generator;
  9 
 10         public MultiSelectTagHelper(IHtmlGenerator generator)
 11         {
 12             this.generator = generator;
 13         }
 14 
 15         private const string ForAttributeName = "asp-for";
 16         private const string ValueAttributeName = "asp-value";
 17         private const string ShowItemCountAttributeName = "asp-showItemCount";
 18         private const string DataSourceAttributeName = "asp-dataSource";
 19         private const string ReadonlyAttributeName = "asp-readonly";
 20 
 21         [HtmlAttributeNotBound]
 22         [ViewContext]
 23         public ViewContext ViewContext { get; set; }
 24 
 25         [HtmlAttributeName(ForAttributeName)]
 26         public ModelExpression For { get; set; }
 27 
 28         [HtmlAttributeName(ValueAttributeName)]
 29         public List<string> Value { get; set; }
 30 
 31         [HtmlAttributeName(ShowItemCountAttributeName)]
 32         public int ShowItemCount { get; set; } = 5;
 33 
 34         [HtmlAttributeName(DataSourceAttributeName)]
 35         public Dictionary<string, string> DataSource { get; set; }
 36 
 37         [HtmlAttributeName(ReadonlyAttributeName)]
 38         public bool Readonly { get; set; }
 39 
 40         public override void Process(TagHelperContext context, TagHelperOutput output)
 41         {
 42             MicroStrutLibraryExceptionHelper.IsNull(context, this.GetType().FullName, LogLevel.Error, "context參數值為空");
 43             MicroStrutLibraryExceptionHelper.IsNull(output, this.GetType().FullName, LogLevel.Error, "output");
 44 
 45             output.TagName = "div";
 46             //output.Attributes.Add("class", "multiselect-drop");
 47 
 48             MultiSelectList selectList = new MultiSelectList(this.DataSource, "Key", "Value", this.Value);
 49 
 50             HtmlContentBuilder builder = new HtmlContentBuilder();
 51 
 52             string id;
 53             if (For == null)
 54             {
 55                 id = output.Attributes["id"].Value.ToString();
 56                 output.Attributes.Remove(output.Attributes["id"]);
 57 
 58                 string options = string.Empty;
 59                 foreach (SelectListItem item in selectList)
 60                 {
 61                     options += $"<option value=\"{item.Value}\" {(item.Selected ? "selected" : "")}>{item.Text}</option>";
 62                 }
 63 
 64                 builder.AppendHtml($"<select id=\"{id}\" name=\"{id}\" multiple=\"multiple\">{options}</select>");
 65             }
 66             else
 67             {
 68                 id = For.Name;
 69 
 70                 TagBuilder dropDown = generator.GenerateSelect(ViewContext, For.ModelExplorer, For.Name, string.Empty, selectList, true, null);
 71                 builder.AppendHtml(dropDown);
 72             }
 73 
 74             string readOnly = "";
 75             if (this.Readonly)
 76             {
 77                 readOnly = $"$('#{id} +div > button').attr('disabled', true);";
 78             }
 79             string script = string.Format(@"
 80 <script>
 81 require(['jquery', 'bootstrap', 'multiselect'], function ($) {{
 82     $(function(){{
 83         $(""#{0}"").multiselect({{
 84             nonSelectedText: '請選擇',
 85             enableFiltering: true,//是否顯示過濾
 86             filterPlaceholder: '查找',
 87             includeSelectAllOption: true,//是否顯示全選
 88             selectAllText: '全選',
 89             numberDisplayed: {1}//顯示條數
 90         }});
 91         {2}
 92     }});
 93 }});
 94 </script>
 95 ", id, this.ShowItemCount, readOnly);
 96 
 97             builder.AppendHtml(script);
 98 
 99             output.Content.AppendHtml(builder);
100 
101             base.Process(context, output);
102         }
103     }

 

為了在編寫程序時不會出錯,我們定義了五個常量ForAttributeName 、ValueAttributeName、ShowItemCountAttributeName、DataSourceAttributeName、ReadonlyAttributeName,分別代表MultiSelect標簽的asp-for等Attribute。也就是MultiSelect傳入的數據源、已選項、下拉列表的展示條數、是否只讀等屬性、For屬性。

 

類定義上面[HtmlTargetElement("multiSelect", Attributes = ValueAttributeName)]作用是在multiSelect標簽上生成ValueAttributeName對應值asp-value的Attribute。

屬性上面

[HtmlAttributeName(ValueAttributeName)]

public List<string> Value { get; set; }

的作用是告訴multiSelect標簽,asp-value傳入的應該是個List<string>類型。這里需要注意的是,在各種文檔、教程中,大多數情況下,Attribute傳入的類型都是一些簡單類型,例如字符串、數字等。其實Attribute是可以傳入復雜的類型的,例如我們這里傳入的List<string>。頁面中也就要如下的使用方式:

1 @model Dictionary<string, string>
2 @{ 
3     List<string> values = ViewData.Get<List<string>>("Values");
4 }
5 
6 <multiSelect id="txtInput0" asp-dataSource="@Model" asp-value="@values"></multiSelect>

 

還有一個屬性ViewContext,這個屬性不是顯式傳入的,而是TagHelper創建時,系統自動賦值的,含義是當前的視圖執行上下文Microsoft.AspNetCore.Mvc.Rendering.ViewContext。

[HtmlAttributeNotBound]

[ViewContext]

public ViewContext ViewContext { get; set; }

 

這里還有一個需要重要說明的是For方式public ModelExpression For { get; set; }。普通方式下,multiSelect的Id、Value都是從頁面傳入的,但是For方式是與Model綁定的,如同HtmlHelper的TextBox、TextBoxFor的區別一樣。例如,一個人可能有多個職責,人的職責屬性是DutyList,所有職責的數據DutyDictionary,在cshtml頁面中就按照如下方式寫multiselect:

@model UserInfo

<multiSelect asp-dataSource="@DutyDictionary" asp-for="DutyList"></multiSelect>

 

MultiSelect構造函數中,傳入了一個IHtmlGenerator參數,這個是通過DI容器自動解析出來的,缺省情況下的實現類是DefaultHtmlGenerator。這個類的主要作用是生成各種html標簽。我們在For方式用到了自動生成select標簽的方法。TagBuilder dropDown = generator.GenerateSelect(ViewContext, For.ModelExplorer, string.Empty, For.Name, selectList, true, null);

 

最后就是重寫Process方法。其實這個方法還是比較簡單的,當For方式時,通過For自動生成select;否則就自己寫select。我們其中還寫了一段腳本,腳本中直接引用了jquery的MultiSelect腳本,

require(['jquery', 'bootstrap', 'multiselect'], function ($) {{

這里我們使用systemjs這個通用的javascript模塊加載器,其中的jquery、bootstrap、multiselect都是在system.config中定義的。System.config.js的代碼大體如下,至於為什么這么寫,大家還是搜網上幫助吧:

 1     System.config({
 2         bundles: {
 3         },
 4         paths: {
 5             "external:": externalUrl+"/"
 6         },
 7         map: {
 8             "jquery": "external:lib/jquery/jquery.min.js",
 9             "bootstrap": "external:lib/bootstrap/js/bootstrap.min.js",
10 
11             //--
12             "jquery-ui": "external:lib/jquery/jquery-ui/jquery-ui.bundle.min.js",
13 
14             //--Plugins
15             "multiselect": "external:lib/plugins/multiselect/js/bootstrap-multiselect.min.js"
16 17 },
18         meta: {
19             '*.css': {
20                 loader: 'external:lib/system/css-loader/css.js'
21             },
22             'jquery': {
23                 format: 'global',
24                 exports: 'jQuery'
25             },
26             'bootstrap': {
27                 format: 'global',
28                 deps: ['jquery']
29             },
30             'jquery-ui': {
31                 format: 'global',
32                 deps: ['jquery','./jquery-ui.min.css']
33             },
34             'multiselect': {
35                 format: 'global',
36                 deps: ['../css/bootstrap-multiselect.min.css']
37             },
38 39         },
40         packages: {
41             '/js': {
42                 format: 'cjs',
43                 defaultExtension: 'js'
44             },
45 
46             //externals
47             'external:js': {
48                 format: 'cjs',
49                 defaultExtension: 'js'
50             }
51         }
52     });
53 
54     //amd require
55     window.require = System.amdRequire;

至此,一個基本的Taghelpr就完成了。

在ASP.NET Core MVC中應該使用 TagHelpers 來替換 HtmlHelpers,因為它們更加的簡潔和容易使用。另一個巨大的好處就是依賴注入,在HtmlHelpers中是使用不了的,因為HtmlHelpers 擴展的都是靜態內容。 但TagHelpers是一個公共類,我們可以很容易的在它的構造函數中注入服務。

 

進階:資源性視圖的應用

按照上節的慣例,我們依舊還一個進階,說明下在TagHelper中如何使用cshtml,以及cshtml作為嵌入的資源該如何寫。

我們從上面MultiSelectTagHelper類中將Process方法的頁面代碼拼接程序提取出來,寫成cshtml如下

 1 @{
 2     string id = ViewData["Id"].ToString();
 3     int showItemCount = (int)ViewData["ShowItemCount"];
 4     bool isReadonly = (bool)ViewData["Readonly"];
 5 }
 6 
 7 <script>
 8     require(['jquery', 'bootstrap', 'multiselect'], function ($) {
 9         $(function(){
10             $("#@id").multiselect({
11                 nonSelectedText: '請選擇',
12                 enableFiltering: true,//是否顯示過濾
13                 filterPlaceholder: '查找',
14                 includeSelectAllOption: true,//是否顯示全選
15                 numberDisplayed: @(showItemCount),//顯示條數
16                 selectAllText: '全選'
17             });
18             @if (isReadonly)
19         {
20             @:$("#@id +div > button").attr("disabled", true);
21                     }
22         });
23     });
24 </script>

 

這樣MultiSelectTagHelper類中就簡化成如下:

 1     [HtmlTargetElement("multiSelect", Attributes = ForAttributeName)]
 2     [HtmlTargetElement("multiSelect", Attributes = ValueAttributeName)]
 3     [HtmlTargetElement("multiSelect", Attributes = ShowItemCountAttributeName)]
 4     [HtmlTargetElement("multiSelect", Attributes = DataSourceAttributeName)]
 5     [HtmlTargetElement("multiSelect", Attributes = ReadonlyAttributeName)]
 6     public class MultiSelectTagHelper : TagHelper
 7     {
 8         private readonly IHtmlGenerator generator;
 9         private readonly IUrlHelperFactory factory;
10         private readonly IHtmlHelper htmlHelper;
11 
12         public MultiSelectTagHelper(IHtmlHelper htmlHelper, IHtmlGenerator generator)
13         {
14             this.generator = generator;
15             this.factory = factory;
16             this.htmlHelper = htmlHelper;
17         }
18 
19         private const string ForAttributeName = "asp-for";
20         private const string ValueAttributeName = "asp-value";
21         private const string ShowItemCountAttributeName = "asp-showItemCount";
22         private const string DataSourceAttributeName = "asp-dataSource";
23         private const string ReadonlyAttributeName = "asp-readonly";
24 
25         [HtmlAttributeNotBound]
26         [ViewContext]
27         public ViewContext ViewContext { get; set; }
28 
29         [HtmlAttributeName(ForAttributeName)]
30         public ModelExpression For { get; set; }
31 
32         [HtmlAttributeName(ValueAttributeName)]
33         public List<string> Value { get; set; }
34 
35         [HtmlAttributeName(ShowItemCountAttributeName)]
36         public int ShowItemCount { get; set; } = 5;
37 
38         [HtmlAttributeName(DataSourceAttributeName)]
39         public Dictionary<string, string> DataSource { get; set; }
40 
41         [HtmlAttributeName(ReadonlyAttributeName)]
42         public bool Readonly { get; set; }
43 
44         public override void Process(TagHelperContext context, TagHelperOutput output)
45         {
46             MicroStrutLibraryExceptionHelper.IsNull(context, this.GetType().FullName, LogLevel.Error, "context參數值為空");
47             MicroStrutLibraryExceptionHelper.IsNull(output, this.GetType().FullName, LogLevel.Error, "output");
48 
49             output.TagName = "div";
50             //output.Attributes.Add("class", "multiselect-drop");
51 
52             MultiSelectList selectList = new MultiSelectList(this.DataSource, "Key", "Value", this.Value);
53 
54             HtmlContentBuilder builder = new HtmlContentBuilder();
55 
56             string id;
57             if (For == null)
58             {
59                 id = output.Attributes["id"].Value.ToString();
60                 output.Attributes.Remove(output.Attributes["id"]);
61 
62                 string options = string.Empty;
63                 foreach (SelectListItem item in selectList)
64                 {
65                     options += $"<option value=\"{item.Value}\" {(item.Selected ? "selected" : "")}>{item.Text}</option>";
66                 }
67 
68                 builder.AppendHtml($"<select id=\"{id}\" name=\"{id}\" multiple=\"multiple\">{options}</select>");
69             }
70             else
71             {
72                 id = For.Name;
73 
74                 TagBuilder dropDown = generator.GenerateSelect(ViewContext, For.ModelExplorer, null, For.Name, selectList, true, null);
75                 builder.AppendHtml(dropDown);
76             }
77 
78             output.Content.AppendHtml(builder);
79 
80             //Contextualize the html helper
81             (htmlHelper as IViewContextAware).Contextualize(ViewContext);
82 
83             ViewDataDictionary data = new ViewDataDictionary(this.ViewContext.ViewData);
84             data["Id"] = id;
85             data["ShowItemCount"] = this.ShowItemCount;
86             data["Readonly"] = this.Readonly;
87             
88             var content = htmlHelper.Partial("TagHelpers/MultiSelect/MultiSelect", data);
89             output.Content.AppendHtml(content);
90 
91             base.Process(context, output);
92         }
93     }
94 }

 

大家可能注意到構造函數中我們增加了個參數IHtmlHelper htmlHelper這個參數是之前MVC的HtmlHelper,我們通過DI方式直接獲取到htmlhelper。然而,此時DI獲取的htmlhelper還無法使用,必須通過(htmlHelper as IViewContextAware).Contextualize(ViewContext);將上下文信息傳入HtmlHelper。var content = htmlHelper.Partial("TagHelpers/MultiSelect/MultiSelect", data); output.Content.AppendHtml(content);這兩句話執行cshtml頁面,將最終頁面的內容呈現在TagHelper中。

這里還有一個問題,就是我們將所有的控件都存放到一個應用程序集中,控件的cshtml頁面也會以資源方式打包進應用程序集中。我們控件的項目結構如下:

MultiSelect的內容如下,有2個文件,一個cshtml,一個是taghelper程序。其他目錄的結構也是類似的。

新的.net core的嵌入資源方式需要在project.json中按照如下方式編寫:

"buildOptions": {

"embed": [ "Components/**/*.cshtml", "TagHelpers/**/*.cshtml" ]

}

這里的意思是我們將所有components和taghelpers目錄下的第二級子目錄下的所有cshtml文件以嵌入方式打包進應用程序集中。在.net core中使用應用程序集中嵌入的文件,還算是比較方便。因為.net core已經把許多可擴展的內容開放出來了。

我們這里寫了一個擴展方法,在RazorViewEngineOptions(RazorViewEngine程序方式的配置)中增加一個Razor視圖文件的定位器EmbeddedFileProvider。EmbeddedFileProvider就可以獲取應用程序集中嵌入的cshtml文件,構造函數第一個參數是包含嵌入cshtml文件的應用程序集,第二個參數是命名空間。

 1     public static class EmbeddedViewServiceCollectionExtensions
 2     {
 3         public static IServiceCollection AddEmbeddComponentView(this IServiceCollection services)
 4         {
 5             if (services == null)
 6             {
 7                 throw new ArgumentNullException(nameof(services));
 8             }
 9 
10             EmbeddedFileProvider fileProvider = new EmbeddedFileProvider(typeof(EmbeddedViewServiceCollectionExtensions).GetTypeInfo().Assembly, "MicroStrutLibrary.Presentation.Web.Controls");
11 
12             services.Configure<RazorViewEngineOptions>(options => {
13                 options.FileProviders.Add(fileProvider);
14             });
15 
16             return services;
17         }
18     }

 

接下來就是在Startup.cs中使用這個擴展方法:

1 public void ConfigureServices(IServiceCollection services)
2 {
3      services.AddMvc(options =>
4         {
5 6         });
7 
8     services.AddEmbeddComponentView();
9 }

 

現在才發現,其實生成select標簽部分也是應該放到csthml中的,而不是在taghelper中生成,就不改了啊:)。

這里主要有幾個小技巧再提示下:

1、cshtml頁面中,Taghelper的Attribute可以傳入各種復雜對象,而不是string\int\bool等簡單類型

2、TagHelper如果使用cshtml,則應該使用IHtmlHelper

3、嵌入資源方式的cshtml,需要使用embeddedfileprovider。

 

面向雲的.net core開發框架


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM