نوشتن TagHelperهای سفارشی برای ASP.NET Core
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» با مفهوم جدید Tag Helpers و همچنین نحوه‌ی استفاده‌ی از نمونه‌های پیش فرض و توکار آن در ASP.NET Core آشنا شدیم. در ادامه قصد داریم با نحوه‌ی پیاده سازی نمونه‌های سفارشی آن‌ها نیز آشنا شویم.


نوشتن یک Tag Helper سفارشی، برای رندر کردن لیست‌های بوت استرپی

فرض کنید می‌خواهیم یک tag helper جدید را جهت رندر کردن لیست بوت استرپی ذیل تهیه کنیم:
<ul class="list-group"> 
  <li class="list-group-item">Item 1</li> 
  <li class="list-group-item">Item 2</li> 
  <li class="list-group-item">Item 3</li> 
</ul>
برای اینکار یک کتابخانه‌ی جدید را به پروژه‌ی جاری اضافه کرده و سپس وابستگی‌های ذیل را نیز به آن اضافه می‌کنیم. این‌ها حداقل‌هایی هستند که جهت دسترسی به امکانات MVC و Tag Helpers، در یک پروژه‌ی مجزای Class library نیاز داریم:
{
  "version": "1.0.0-*",
 
    "dependencies": {
        "NETStandard.Library": "1.6.0",
        "Microsoft.AspNetCore.Http.Extensions": "1.0.0",
        "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.1",
        "Microsoft.AspNetCore.Mvc.Core": "1.0.1",
        "Microsoft.AspNetCore.Mvc.ViewFeatures": "1.0.1",
        "Microsoft.AspNetCore.Razor.Runtime": "1.0.0"
    },
 
  "frameworks": {
    "netstandard1.6": {
      "imports": "dnxcore50"
    }
  }
}


بررسی آناتومی یک کلاس TagHelper

یک کلاس Tag Helper سفارشی، در حالت کلی می‌تواند شکل زیر را داشته باشد:
namespace Core1RtmEmptyTest.TagHelpers
{
    [HtmlTargetElement("list-group")]
    public class ListGroupTagHelper : TagHelper
    {
        [HtmlAttributeName("asp-items")]
        public List<string> Items { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
        }
    }
}
در اینجا نام کلاس، به TagHelper ختم می‌شود و همچنین این کلاس از کلاس پایه‌ی TagHelper ارث بری می‌کند. ذکر HtmlTargetElement الزامی بوده و در صورت عدم تعریف آن، TagHelper تعریف شده توسط ASP.NET Core شناسایی و بارگذاری نخواهد شد.
توسط HtmlTargetElement نام نهایی تگ مرتبط با TagHelper سفارشی را تعریف و سفارشی سازی کرده‌ایم. در این حالت این TagHelper جدید در Viewهای برنامه، توسط تگ ذیل شنایی می‌شود (بجای نام پیش فرض کلاس):
 <list-group></list-group>
همچنین در اینجا، یک خاصیت عمومی نیز تعریف شده‌است. تمام خواص عمومی تعریف شده‌ی در اینجا به صورت ویژگی‌هایی در تگ نهایی TagHelper قابل دسترسی و مقدار دهی خواهند بود:
 <list-group asp-items="Model.Items"></list-group>
 برای لغو این حالت می‌توان از ویژگی HtmlAttributeNotBound استفاده کرد.
برای اینکه نام این ویژگی را نیز بتوانیم سفارشی سازی کنیم، می‌توان از ویژگی HtmlAttributeName استفاده کرد. در صورت عدم ذکر آن، از نام پیش فرض این خاصیت عمومی جهت تعریف ویژگی‌های تگ نهایی استفاده می‌گردد.
عملیات نهایی افزودن تگ‌های HTML، به View برنامه، در متد Process انجام می‌شود. در اینجا توسط متدهایی مانند output.Content.AppendHtml می‌توان خروجی دلخواهی را به صفحه اضافه کرد.


تکمیل کدهای Tag Helper سفارشی رندر کردن لیست‌های بوت استرپی

پس از آشنایی با ساختار کلی یک کلاس TagHelper، اکنون می‌توان کدهای آن را به نحو ذیل تکمیل کرد:
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace Core1RtmEmptyTest.TagHelpers
{
    [HtmlTargetElement("list-group")]
    public class ListGroupTagHelper : TagHelper
    {
        [HtmlAttributeName("asp-items")]
        public List<string> Items { get; set; }
 
        protected HttpRequest Request => ViewContext.HttpContext.Request;
 
        [ViewContext, HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
 
            if (output == null)
            {
                throw new ArgumentNullException(nameof(output));
            }
 
            if (Items == null)
            {
                throw new InvalidOperationException($"{nameof(Items)} must be provided");
            }
 
            output.TagName = "ul";
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Attributes.Add("class", "list-group");
 
            foreach (var item in Items)
            {
                TagBuilder itemBuilder = new TagBuilder("li");
                itemBuilder.AddCssClass("list-group-item");
                itemBuilder.InnerHtml.Append(item);
                output.Content.AppendHtml(itemBuilder);
            }
        }
    }
}
توضیحات:
 - چون می‌خواهیم تگ نهایی آن، list-group نام داشته باشد، آن‌را توسط ویژگی HtmlTargetElement به صورت صریحی مشخص کرده‌ایم.
 - همچنین علاقمندیم تا ویژگی دریافت لیست آیتم‌ها، نامی معادل asp-items داشته باشد. بنابراین آن‌را نیز توسط ویژگی HtmlAttributeName، دقیقا مشخص کرده‌ایم.
 - در این کلاس، یک خاصیت اضافه‌ی ViewContext را نیز مشاهده می‌کنید. ویژگی ViewContext اعمالی به آن، سبب خواهد شد تا اطلاعات درخواست جاری، به این خاصیت عمومی، به صورت خودکار تزریق شود. بنابراین اگر نیاز به اطلاعاتی مانند Request جاری دارید، شیء ViewContext.HttpContext.Request، این مقادیر را در اختیار شما قرار می‌دهد. به علاوه اگر دقت کرده باشید، این خاصیت با ویژگی HtmlAttributeNotBound مزین شده‌است. از این جهت که نمی‌خواهیم این خاصیت عمومی، در لیست ویژگی‌های تگ نهایی TagHelper در حال تهیه، ظاهر شود.
 - پس از آن کاری که انجام شده، تکمیل متد Process است. در اینجا توسط output.TagName مشخص می‌کنیم که TagHelper جاری، در بین تگ‌های ul قرار گیرد (مفهوم TagMode.StartTagAndEndTag ذکر شده) و همچنین این تگ محصور کننده دارای کلاس list-group بوت استرپ نیز خواهد بود.
 - سپس بر روی لیست آیتم‌های دریافت شده، یک حلقه را تشکیل داده و به کمک TagBuilder، تگ‌های li داخل ul برونی را تکمیل می‌کنیم. این TagBuilder در نهایت توسط متد output.Content.AppendHtml به View برنامه اضافه خواهد شد. در اینجا، متد Append هم وجود دارد. اگر از آن استفاده شود، خروجی نهایی HTML Encoded خواهد بود.
 

ثبت و استفاده‌ی از TagHelper سفارشی تهیه شده

پس از تعریف TagHelper سفارشی فوق، ابتدا ارجاعی از اسمبلی آن‌را به پروژه‌ی جاری اضافه می‌کنیم و سپس به فایل ViewImports.cshtml_ برنامه مراجعه و یک سطر ذیل را به آن اضافه می‌کنیم:
 @addTagHelper *,Core1RtmEmptyTest.TagHelpers
در اینجا عبارت Core1RtmEmptyTest.TagHelpers همان نام فضای نام اصلی پروژه‌ی Class library دربرگیرنده‌ی TagHelper سفارشی است.
اکنون که این TagHelper در Viewهای برنامه قابل شناسایی است، روش افزودن آن، بر اساس همان سفارشی سازی‌هایی است که انجام دادیم:

  • #
    ‫۷ سال و ۱۲ ماه قبل، پنجشنبه ۸ مهر ۱۳۹۵، ساعت ۲۰:۱۸
    در خروجی نهایی خود تگ list-group هم وجود خواهد داشت یا محتوای متد process جایگزینش می‌شود ؟ ( منظور مانند حالت transclusion که در انگولار یک وجود داشت ).
    دلیل این سوالم این هست که در استفاده‌ی ترکیبی با مثلن انگولار آیا باید مواظب تداخل‌های اسمی بود یا نه .
    • #
      ‫۷ سال و ۱۲ ماه قبل، پنجشنبه ۸ مهر ۱۳۹۵، ساعت ۲۱:۵۸
      تگ جدید list-group صرفا server side است و در زمان ارائه‌ی نهایی View به صورت HTML، با منطق پیاده سازی شده‌ی در متد Process جایگزین می‌شود.
  • #
    ‫۷ سال و ۱۱ ماه قبل، دوشنبه ۱۲ مهر ۱۳۹۵، ساعت ۲۲:۴۴
    یک نکته: برای تبدیل محتوای یک TagBuilder به رشته می‌توان از متد زیر استفاده کرد:
    using System.IO;
    using System.Text.Encodings.Web;
    using Microsoft.AspNetCore.Html;
    
    namespace Sample
    {
        /// <summary>
        /// Html Helper Extensions
        /// </summary>
        public static class HtmlHelperExtensions
        {
            /// <summary>
            /// Convert IHtmlContent/TagBuilder to string
            /// </summary>
            public static string GetString(this IHtmlContent content)
            {
                using (var writer = new StringWriter())
                {
                    content.WriteTo(writer, HtmlEncoder.Default);
                    return writer.ToString();
                }
            }
        }
    }
  • #
    ‫۷ سال و ۷ ماه قبل، شنبه ۳۰ بهمن ۱۳۹۵، ساعت ۱۸:۰۷
    یک نکته‌ی تکمیلی: به روز رسانی یک Tag Helper از طریق Ajax

    فرض کنید قسمتی از صفحه را با یک Tag Helper سفارشی ایجاد کرده‌اید. اگر بخواهید یک دکمه‌ی به روز رسانی را هم در اینجا اضافه کنید تا به صورت Ajax ایی این قسمت به روز شود، نیاز است بتوان این تگ هلپر را مجددا تولید کرد و سپس به صورت ()return Content بازگشت داد.
    برای اینکار قسمتی که سبب رندر مجدد یک تگ هلپر می‌شود، به صورت زیر قابل پیاده سازی است:
    var tagHelper = new MyCustomTagHelper();
    
    var tagHelperContext = new TagHelperContext(
        allAttributes: new TagHelperAttributeList(),
        items: new Dictionary<object, object>(),
        uniqueId: Guid.NewGuid().ToString("N"));
     
    var tagHelperOutput = new TagHelperOutput(
        tagName: "div",
        attributes: new TagHelperAttributeList(),
        getChildContentAsync: (useCachedResult, encoder) =>
        {
            var tagHelperContent = new DefaultTagHelperContent();
            tagHelperContent.SetContent(string.Empty);
            return Task.FromResult<TagHelperContent>(tagHelperContent);
        });
     
    tagHelper.Process(tagHelperContext, tagHelperOutput);
    var content = tagHelperOutput.Content.GetContent();
  • #
    ‫۷ سال و ۷ ماه قبل، جمعه ۶ اسفند ۱۳۹۵، ساعت ۱۲:۴۵
    نکته‌ای در مورد نحوه‌ی ثبت TagHelper‌های تهیه شده
    نامی که برای ثبت یک TagHelper یا مجموعه‌ای از آن‌ها در فایل ViewImports باید درج شود، دقیقا نام اسمبلی دربرگیرنده‌ی آن‌ها است و نه نام فضای نام کلاس‌های مرتبط. برای مثال اگر dll تولیدی، core-resources.dll نام دارد و فضای نام آن core_resources است، برای تعریف و ثبت آن باید نوشت (استفاده از نام اسمبلی):
    @addTagHelper *, core-resources
    یا می‌توان نام فایل خروجی را در فایل project.json سفارشی سازی کرد:
     "buildOptions": {
            "outputName": "core_resources"
        },
    و سپس از این نام dll جدید تولیدی استفاده کرد:
    @addTagHelper *, core_resources
  • #
    ‫۷ سال و ۲ ماه قبل، یکشنبه ۱۱ تیر ۱۳۹۶، ساعت ۱۵:۳۶
    من در حین ایجاد tag helper سفارشی برای منو‌ها به مشکلی برخوردم
    برای ایجاد لینک‌های صفحه از url.Action  به صورت زیر استفاده میکنم: 
      Menu.ChildsList.Add(new ChildMenu()
                    {
                        Text = "داشبورد",
                        Url = url.Action(new UrlActionContext() { Action = "Index", Controller = "Home"})
                    });
     اما در کلاس UrlActionContext پارامتری برای مشخص کردن Area وجود نداره! 
    • #
      ‫۷ سال و ۲ ماه قبل، یکشنبه ۱۱ تیر ۱۳۹۶، ساعت ۱۶:۲۴
      این مورد مانند قبل است (همانند ASP.NET MVC 5.x) که در آن از anonymous objects و مشخص سازی دستی area استفاده می‌شود:
      var urlHelper = ViewContext.HttpContext.Items.Values.OfType<IUrlHelper>().FirstOrDefault();
      و سپس
      // How to inject the ViewContext automatically
      [ViewContext, HtmlAttributeNotBound]
      public ViewContext ViewContext { get; set; }
      
      // How to use the injected ViewContext
      IUrlHelper urlHelper = new UrlHelper(ViewContext);
      var actionUrl = urlHelper.Action(action: nameof(MyController.Xyz),
                      controller: nameof(MyController).Replace("Controller", string.Empty),
                      values:
                      new
                      {
                         //...,
                          area = "SomeName"
                      });
      • #
        ‫۷ سال و ۲ ماه قبل، یکشنبه ۱۱ تیر ۱۳۹۶، ساعت ۱۷:۰۴
        از این کلاس هم استفاده میکنم  area رو نمیشناسه:
        IUrlHelper urlHelper = new UrlHelper(_actionContextAccessor.ActionContext); 
        adminMenu.ChildsList.Add(new ChildMenu()
                        {
                            Text = "مدیریت کاربران",
                            Url = urlHelper.Action("Index","UserManager",values:new{area:"Identity"})
                        });

  • #
    ‫۶ سال و ۸ ماه قبل، پنجشنبه ۲۱ دی ۱۳۹۶، ساعت ۰۴:۱۷
    با سلام،
    همانطور که می‌دانید helper@ در حال حاضر در asp.net core 2 پشتیبانی نمی‌شود.
    من می‌خواهم قطعه کد ذیل را که در asp.net mvc 5 نوشته شده را مطابق با asp.net core 2 بازنویسی کنم.
    چگونه می‌توانم آن را انجام دهم؟
    //MyHelpers.cshtml:
    //Recursive function for rendering child nodes for the specified node
    
    @helper CreateNavigation(int parentId, int depthNavigation, int currentPageId)
    {
       @MyHelpers.Navigation(parentId, depthNavigation, currentPageId);
    }
    
    @helper Navigation(int parentId, int depthNavigation, int currentPageId)
    {
    
       if ()
       {
           if ()
           {
             <ul style="">
                @foreach ()
                {
                   if ()
                    {
                       <li class="">
                          @Navigation(child.Id, depthNavigation, currentPageId)
                       </li>
                    }
                 }
             </ul>
          }
       }
    }
    //I call the method in _Menu.cshtml:
     @MyHelpers.CreateNavigation(rootNode.Id, 2,currentPageId);

  • #
    ‫۶ سال و ۴ ماه قبل، جمعه ۱۴ اردیبهشت ۱۳۹۷، ساعت ۰۱:۱۹
    در فایل _viewimport زمانی که مسیر کامل namespace رو میدادم intelisense کار نمی‌کرد
     @addTagHelper *,KanalexUI.Classes.TagHelpers
    وقتی فقط namespace اصلی پروژه رو نوشتم شناخت
     @addTagHelper *, KanalexUI
    از .net core 2.0 استفاده می‌کنم
    https://stackoverflow.com/questions/48271514/custom-tag-helper-not-working 
  • #
    ‫۵ سال و ۶ ماه قبل، یکشنبه ۵ اسفند ۱۳۹۷، ساعت ۱۴:۱۰
    پیاده سازی  TagHelper سفارشی ImageGravatar  جهت نمایش آواتار کاربر از سایت gravatar 


    پیاده سازی کلاس GarvatarTagHelper
    [HtmlTargetElement("img-gravatar")]
        public class GravatarTagHelper : TagHelper
        {
            [HtmlAttributeName("email")]
            public string Email { get; set; }
    
            [HtmlAttributeName("alt")]
            public string Alt { get; set; }
    
            [HtmlAttributeName("class")]
            public string Class { get; set; }
    
            [HtmlAttributeName("size")]
            public int Size { get; set; }
    
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                if (!string.IsNullOrWhiteSpace(Email))
                {
                    var hash = Md5HashHelper.GetHash(Email);
    
                    output.TagName = "img";
                    if (!string.IsNullOrWhiteSpace(Class))
                    {
                        output.Attributes.Add("class", Class); 
                    }
    
                    if (!string.IsNullOrWhiteSpace(Alt))
                    {
                        output.Attributes.Add("alt", Alt);
                    }
                    
                    output.Attributes.Add("src", GetAvatarUrl(hash, Size));
                    output.TagMode = TagMode.SelfClosing;
                } 
            }
    
            private static string GetAvatarUrl(string hash, int size)
            {
                var sizeArg = size > 0 ? $"?s={size}" : "";
    
                return $"https://www.gravatar.com/avatar/{hash}{sizeArg}";
            }
     
    استفاده از TagHepler  با مقدار  Email فرد در View مورد نظر
     <img-gravatar email="@Model.Email" class="img-thumbnail" size="150" />