ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هشت دقیقه

یکی دیگر از تغییرات مهم Razor در ASP.NET Core، معرفی Tag Helpers است که همانند HTML Helpers نگارش‌های پیشین ASP.NET MVC، کار رندر کردن HTML را انجام می‌دهند و در اغلب موارد می‌توان آن‌ها را جایگزین HTML Helpers کرد. مزیت استفاده‌ی از Tag helpers، شبیه بودن آن‌ها به المان‌ها و ویژگی‌های HTML است. در کل اینکه باید از HTML Helpers استفاده کرد و یا از Tag Helpers، بیشتر یک انتخاب شخصی و سلیقه‌ای است.


فعال سازی استفاده‌ی از Tag Helpers برای تمام Viewهای برنامه

برای اینکه تمام Viewهای سایت بتوانند به امکانات Tag Helpers دسترسی پیدا کنند، باید یک سطر ذیل را به فایل ViewImports.cshtml_ اضافه کرد:
 @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
در اینجا * به معنای استفاده‌ی از تمام Tag Helpers موجود در اسمبلی ذکر شده‌است.

Microsoft.AspNetCore.Mvc.TagHelpers به همراه افزودن وابستگی Microsoft.AspNetCore.Mvc در حین فعال سازی ASP.NET MVC، به پروژه اضافه می‌شود:



فعال سازی Intellisense مربوط به Tag Helpers در ویژوال استودیو

هرچند فعال سازی ASP.NET MVC، تنها وابستگی است که برای کار با Tag Helpers نیاز است، اما برای فعال سازی Intellisense آن‌ها باید بسته‌ی Microsoft.AspNetCore.Razor.Tools را نیز به فایل prject.json برنامه، جهت نصب معرفی کرد:
{
    "dependencies": {
         //same as before
         "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0",
         "Microsoft.AspNetCore.Razor.Runtime": "1.0.0",
         "Microsoft.AspNetCore.Razor.Tools": {
            "version": "1.0.0-preview2-final",
            "type": "build"
        }
    },
 
    "tools": {
         //same as before
        "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
    } 
}
ضمنا اگر از ReSharper استفاده می‌کنید (تا نگارش resharper-2016.1)، فعلا مجبور هستید که آن‌را غیرفعال کنید. اطلاعات بیشتر


یک مثال: ایجاد لینکی به یک اکشن متد
 <a asp-controller="Home" asp-action="Index" asp-route-id="123">Home</a>
در اینجا نحوه‌ی ایجاد لینکی را مشاهده می‌کنید که به کنترلر Home و اکشن متد Index آن اشاره می‌کند. این syntax جدید، جایگزین ActionLink مربوط به HTML Helperها است. در اینجا asp-route-id را نیز مشاهده می‌کنید. قسمت asp-route آن جهت مقدار دهی پارامترهای مسیریابی است و قسمت id- بنابر نام پارامتری که قرار است مقدار دهی شود، متغیر خواهد بود.
اگر نیاز به اشاره‌ی به مسیریابی خاصی از طریق نام آن وجود دارد (همان نام‌هایی که در حین تعریف یک مسیریابی ذکر می‌شوند) می‌توان به صورت ذیل عمل کرد:
 <a asp-route="login">Login</a>
و یا برای مشخص سازی پروتکل خاصی و یا ذکر دقیق نام هاست، می‌توان از روش زیر استفاده کرد:
 <a asp-controller="Account"
   asp-action="Register"
   asp-protocol="https"
   asp-host="asepecificdomain.com"
   asp-fragment="fragment">Register</a>


راهنمای تبدیل HTML Helpers به Tag Helpers

در جدول ذیل، مثال‌هایی را از HTML Helpers متداول و معادل‌های Tag Helper آن‌ها مشاهده می‌کنید:

Tag Helper
HTML Helper
<label asp-for="Email" class="col-md-2 control-label"></label>
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<a asp-controller="MyController" asp-action="MyAction" 
class="my-css-classname" my-attr="my-attribute">Click me</a>
@Html.ActionLink("Click me", "MyController", "MyAction", 
{ @class="my-css-classname", data_my_attr="my-attribute"})
<input asp-for="FirstName" style="width:100px;"/>
@Html.TextBox("FirstName", Model.FirstName, new { style = "width: 100px;" })
<input asp-for="Email" class="form-control" />
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
<input asp-for="Password" class="form-control" />
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
<input asp-for="UserName" class="form-control" />
@Html.EditorFor(l => l.UserName,
 new { htmlAttributes = new { @class = "form-control" } })
<form asp-controller="Account" asp-action="Register" 
method="post" class="form-horizontal" role="form">
@using (Html.BeginForm("Register", "Account",
 FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
<span asp-validation-for="UserName" class="text-danger"></span>
@Html.ValidationMessageFor(m => m.UserName, "",
 new { @class = "text-danger" })
<div asp-validation-summary="ValidationSummary.All" class="text-danger"></div>
@Html.ValidationSummary("", new { @class = "text-danger" })


نکات تکمیلی کار با فرم‌ها توسط Tag Helpers

نمونه‌ای از مثال Tag helper کار با فرم‌ها را در جدول فوق ملاحظه می‌کنید. چند نکته‌ی تکمیلی ذیل را می‌توان به آن اضافه کرد:
- در حین کار با Tag Helpers، درج anti forgery token به صورت خودکار صورت می‌گیرد. اگر می‌خواهید که این توکن ذکر نشود، آن‌را توسط ویژگی "asp-anti-forgery="false خاموش کنید.
- برای درج پارامترهای مسیریابی خاص، از asp-route به همراه نام پارامتر مدنظر استفاده کنید:
 <form asp-controller="Account"
      asp-action="Login"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      method="post" >
</form>
که در نهایت به یک چنین حالتی رندر می‌شود
 <form action="/Account/Login?returnurl=%2FHome%2FAbout" method="post">
- همانند action linkها در اینجا نیز برای اشاره‌ی به یک مسیریابی از طریق نام آن می‌توان از ویژگی asp-route استفاده کرد
 <form asp-route="login"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      method="post" >
</form>


Tag helpers مخصوص تعریف اسکریپت‌ها و CSSها

 در اینجا Tag Helpers صرفا به عنوان جایگزین‌های HTML Helpers مطرح نیستند. توسط آن‌ها قابلیت‌های جدیدی نیز ارائه شده‌است. برای مثال اگر تگ اسکریپت را به صورت ذیل تعریف کنیم:
 <script asp-src-include="~/app/**/*.js"></script>
یک چنین خروجی فرضی را تولید می‌کند:
 <script src="/app/app.js"></script>
<script src="/app/controllers/controller1.js"></script>
<script src="/app/controllers/controller2.js"></script>
<script src="/app/controllers/controller3.js"></script>
<script src="/app/controllers/controller4.js"></script>
<script src="/app/services/service1.js"></script>
<script src="/app/services/service2.js"></script>
به این معنا که یک سطر asp-src-include، بر اساس الگویی که دریافت می‌کند، تمام فایل‌های اسکریپت موجود در یک پوشه را یافته و برای آن‌ها، تگ اسکریپت تولید می‌کند. دراینجا ذکر ** به معنای بررسی تمام زیرپوشه‌های app است. اگر تنها پوشه‌ی خاصی مدنظر است، باید ** را حذف کرد.
در این بین اگر می‌خواهید از پوشه‌ی خاصی صرفنظر کنید، از asp-src-exclude استفاده کنید:
 <script asp-src-include="~/app/**/*.js"
        asp-src-exclude="~/app/services/**/*.js">
</script>
همچنین در اینجا امکان تعریف CDN و fallback هم وجود دارد. استفاده‌ی از CDNها جهت کاهش ترافیک سرور و بهبود کارآیی برنامه با ارائه‌ی نمونه‌های کش شده‌ی فریم ورک‌های معروف، متداول هستند که در اینجا نمونه‌ای از نحوه‌ی تعریف آن‌ها را مشاهده می‌کنید. همچنین تعریف fallback در اینجا به این معنا است که اگر CDN در دسترس نبود، به نمونه‌ی محلی موجود بر روی سرور مراجعه شود.
 <link rel="stylesheet" href="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css/bootstrap.min.css"
      asp-fallback-href="~/lib/bootstrap/css/bootstrap.min.css"
      asp-fallback-test-class="hidden"
      asp-fallback-test-property="visibility"
      asp-fallback-test-value="hidden" />
 
<script src="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/bootstrap.min.js"
        asp-fallback-src="~/lib/bootstrap/js/bootstrap.min.js"
        asp-fallback-test="window.jQuery">
</script>

به علاوه اگر ویژگی asp-file-version را نیز ذکر کنید:
 <link rel="stylesheet" href="~/css/site.min.css" asp-file-version="true"/>
یک چنین لینکی تولید می‌شود:
 <link rel="stylesheet" href="/css/site.min.css?v=UdxKHVNJA5vb1EsG9O9uURFDfEE3j1E3DgwL6NiDGMc" />
هدف آن نیز اصطلاحا cache busting است. به این معنا که با تغییر محتوای این فایل‌ها، کوئری استرینگ تولید شده، مجددا محاسبه شده و مرورگر همواره آخرین نگارش موجود را دریافت خواهد کرد و دیگر از نمونه‌ی کش شده‌ی قدیمی استفاده نمی‌کند.

یک نکته: ویژگی asp-file-version را برای تصاویر هم می‌توان بکار برد:
 <img src="~/images/logo.png"
     alt="company logo"
     asp-file-version="true" />
که یک چنین خروجی را تولید می‌کند و هدف آن نیز جلوگیری از کش شدن تصویر، با تغییر محتوای آن است:
 <img src="/images/logo.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk"
     alt="company logo"/>


بررسی Environment Tag Helper

با متغیرهای محیطی و نحوه‌ی تعریف آن‌ها در قسمت‌های قبل آشنا شدیم. در اینجا tag helper سفارشی خاصی برای کار با آن‌ها ارائه شده‌است که شیبه به if/else عمل می‌کنند:
<environment names="Development">    
   <link rel="stylesheet" href="~/css/site1.css" />
   <link rel="stylesheet" href="~/css/site2.css" />
</environment>

<environment names="Staging,Production">
   <link rel="stylesheet" href="~/css/site.min.css" asp-file-version="true"/>
</environment>
هدف این است که اگر متغیر محیطی به Development تنظیم شده بود، لینک‌های ساده و اصلی فایل‌های css یا اسکریپت در HTML نهایی درج شوند و اگر حالت توسعه تنظیم شده بود، لینک‌های min یا فشرده شده‌ی آن‌ها ارائه شوند؛ به همراه asp-file-version که cache busting را فعال می‌کند.


کار با دراپ داون‌ها توسط Tag helpers

فرض کنید ViewModel یک view جهت نمایش یک دراپ داون به این صورت تنظیم شده‌است:
public class CustomerViewModel
{
   public string Vehicle { get; set; }  
   public List<SelectListItem> Vehicles { get; set; }
برای نمایش SelectListItem توسط tag helpers می‌توان به صورت ذیل عمل کرد:
 <select asp-for="Vehicle" asp-items="Model.Vehicles">
</select>
asp-for به نام خاصیتی اشاره می‌کند که در نهایت مقدار انتخاب شده را دریافت می‌کند و asp-items لیست آیتم‌های دراپ داون را رندر می‌کند.
  • #
    ‫۸ سال و ۱ ماه قبل، جمعه ۲۹ مرداد ۱۳۹۵، ساعت ۱۳:۵۴
    یک نکته‌ی تکمیلی
    ReSharper Ultimate 2016.2 مشکلات با ASP.NET Core 1.0 را برطرف کرده‌است.
  • #
    ‫۷ سال و ۹ ماه قبل، یکشنبه ۱۴ آذر ۱۳۹۵، ساعت ۱۹:۰۵
    برای فعال سازی  Intellisense مربوط به Tag Helpers، علاوه بر تنظیمات مطرح شده، ممکن است احتیاج به یک بار بسته و باز کردن دوباره Visual Studio باشد.
  • #
    ‫۷ سال و ۶ ماه قبل، دوشنبه ۷ فروردین ۱۳۹۶، ساعت ۰۰:۲۱
    به روز رسانی
    با حذف فایل project.json در VS 2017، اکنون با کلیک راست بر روی گروه نام پروژه (فایل csproj)، گزینه‌ی Edit آن ظاهر شده و مداخل ذکر شده‌ی در مطلب فوق، چنین تعاریفی را پیدا می‌کنند: 
    <Project Sdk="Microsoft.NET.Sdk.Web">
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="1.1.2" />
        <PackageReference Include="Microsoft.AspNetCore.Razor.Runtime" Version="1.1.1" />
      </ItemGroup>
    
      <ItemGroup>
        <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.0" />
      </ItemGroup>
    </Project>

    همچنین Razor Tools به صورت یک افزونه‌ی مجزا درآمده‌است که از اینجا قابل دریافت می‌باشد (و به صورت پیش فرض در VS 2017 نصب است). اطلاعات بیشتر
  • #
    ‫۶ سال و ۷ ماه قبل، شنبه ۱۴ بهمن ۱۳۹۶، ساعت ۱۳:۳۱
    ارتقاء به ASP.NET Core 2.1

    در ASP.NET Core 2.1 بجای متدهای Html.Partial vs Html.RenderPartial, sync vs async برای رندر partial views می‌توان از Tag helper جدید partial استفاده کرد:
    <partial name="_ProductPartial" asp-for="@Model[i]" />
    که در اینجا name به فایل ProductPartial.cshtml_ اشاره می‌کند و مقدار مدل آن نیز مشخص شده‌است.
  • #
    ‫۴ سال و ۷ ماه قبل، سه‌شنبه ۲۴ دی ۱۳۹۸، ساعت ۲۱:۴۶
    با سلام و تشکر؛ ارسال بیشتر از یک پارامتر از طریق مثلا این tag helper چگونه است؟ (غیر از id چگونه پارامتر دوم با نام type را ارسال کنیم؟)
    <a asp-controller="Home" asp-action="Index" asp-route-id="123">Home</a>
    • #
      ‫۴ سال و ۷ ماه قبل، سه‌شنبه ۲۴ دی ۱۳۹۸، ساعت ۲۲:۳۷
      محدودیتی ندارد. هر چندبار که نیاز بود آن‌را فراخوانی کنید:
      <a asp-controller="User" asp-action="Edit" asp-route-id="@item.Id" asp-route-username="@item.UserName">Edit</a>
  • #
    ‫۳ سال قبل، پنجشنبه ۱۱ شهریور ۱۴۰۰، ساعت ۰۴:۰۶
    نکته تکمیلی
    پر کردن مقدار SelectListItem سمت سرور با متود سفارشی :
    public static class CommonExtensionMethods
    {
        public static List<SelectListItem> CreateSelectListItem<T>(
            this List<T> items,
            object selectedItem = null,
            bool addChooseOneItem = true,
            string firstItemText = "انتخاب کنید",
            string firstItemValue = 0
        )
        {
            var modelType = items.First().GetType();
    
            var idProperty = modelType.GetProperty("Id");
            var titleProperty = modelType.GetProperty("Title");
            if (idProperty is null || titleProperty is null)
                throw new ArgumentNullException(
                    $"{typeof(T).Name} must have ```Id``` and ```Title``` propeties");
    
            var result = new List<SelectListItem>();
            if (addChooseOneItem)
                result.Add(new SelectListItem(firstItemText, firstItemValue));
            foreach (var item in items)
            {
                var id = idProperty.GetValue(item)?.ToString();
                var text = titleProperty.GetValue(item)?.ToString();
                var selected = selectedItem?.ToString() == id;
                result.Add(new SelectListItem(text, id, selected));
            }
    
            return result;
        }
    }  
    نحوه استفاده :
    مدلی که AllMainCategories برگشت میدهد:
    public class ShowCategory
    {
        public int Id { get; set; }
    
        public string Title { get; set; }
    }
    public async Task<IActionResult> Add()
    {
        var categories = await _categoryService.AllMainCategories();
        ViewBag.MainCategories = categories.ToList().CreateSelectListItem(firstItemText: "خودش سر دسته باشد");
        return View();
    }
    
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> Add(AddCategoryViewModel model)
    {
        if (!ModelState.IsValid)
        {
            var categories = await _categoryService.AllMainCategories();
            ViewBag.MainCategories = categories.ToList()
                .CreateSelectListItem(model.ParentId, firstItemText: "خودش سر دسته باشد");
            ModelState.AddModelError(string.Empty, PublicConstantStrings.ModelStateErrorMessage);
            return View(model);
        }
        await _categoryService.AddAsync(new Category()
        {
            Title = model.Title,
            ParentId = model.ParentId == 0 ? null : model.ParentId
        });
        await _uow.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }