Tag Helpers: типізований HTML
Tag Helpers: типізований HTML
Tag Helpers — це механізм що дозволяє збагачувати HTML-елементи C#-логікою через атрибути з префіксом asp-. Замість магічних рядків — типізовані посилання на властивості моделі і маршрути.
Порівняйте:
@* ❌ БЕЗ Tag Helpers: магічні рядки, нема compile-time перевірки *@
<form action="/products/create" method="post">
<input type="text" name="Input.Name" id="Input_Name" value="@Model.Input.Name">
<a href="/products/details?id=@product.Id">Деталі</a>
</form>
@* ✅ З Tag Helpers: типізовано, перевірка при компіляції, генерує той самий HTML *@
<form asp-page="./Create" method="post">
<input asp-for="Input.Name">
<a asp-page="./Details" asp-route-id="@product.Id">Деталі</a>
</form>
Якщо ви перейменуєте властивість Input.Name на Input.Title — Razor-компілятор покаже помилку у всіх asp-for="Input.Name". З магічними рядками — тихий баг.
_ViewImports.cshtml має бути:@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
dotnet new webapp.asp-for: зв'язування з властивістю моделі
asp-for — найважливіший атрибут. Він зв'язує HTML-елемент з властивістю PageModel:
@* PropertyName у PageModel: [BindProperty] public CreateInput Input { get; set; } *@
@* Generates:
name="Input.Name"
id="Input_Name"
type="text"
value="@Model.Input.Name" (при наявності)
+ data-val-* атрибути для client-side validation *@
<input asp-for="Input.Name">
@* Для різних типів — правильний type автоматично *@
<input asp-for="Input.Email"> @* type="email" *@
<input asp-for="Input.Price"> @* type="text" + decimal validation *@
<input asp-for="Input.BirthDate"> @* type="date" *@
<input asp-for="Input.IsActive"> @* type="checkbox" *@
<input asp-for="Input.Quantity" type="number"> @* можна перевизначити type *@
@* textarea *@
<textarea asp-for="Input.Description" rows="5"></textarea>
@* label: генерує правильний for="Input_Name" + текст з DisplayAttribute *@
<label asp-for="Input.Name" class="form-label"></label>
@* Якщо [Display(Name = "Назва товару")] — виведе "Назва товару" *@
@* Без Display — виведе ім'я властивості "Name" *@
@* select: потрібно з asp-items *@
<select asp-for="Input.CategoryId" asp-items="Model.CategoryOptions">
<option value="">— Оберіть категорію —</option>
</select>
asp-for з вкладеними об'єктами
@* PageModel: public Product Product { get; set; } *@
<input asp-for="Product.Name"> @* name="Product.Name" *@
<input asp-for="Product.Category.Id"> @* name="Product.Category.Id" *@
asp-for у циклі
@* PageModel: public List<ProductItem> Items { get; set; } *@
@for (int i = 0; i < Model.Items.Count; i++)
{
<input asp-for="Items[i].Name"> @* name="Items[0].Name", "Items[1].Name"... *@
<input asp-for="Items[i].Price">
}
Навігаційні Tag Helpers: asp-page, asp-route-*
@* Посилання на Razor Page *@
<a asp-page="/Products/Index">Всі товари</a>
@* → href="/products" *@
<a asp-page="./Index">Всі товари</a>
@* Відносний шлях: відносно поточної папки Pages/Products/ *@
@* З параметрами маршруту — asp-route-{paramName} *@
<a asp-page="./Details" asp-route-id="@product.Id">Деталі @product.Name</a>
@* → href="/products/details/42" (якщо @page "{id}") *@
@* → href="/products/details?id=42" (якщо без @page "{id}") *@
@* Кілька параметрів *@
<a asp-page="./Edit"
asp-route-id="@product.Id"
asp-route-returnUrl="@Context.Request.Path">
Редагувати
</a>
@* → /products/edit/42?returnUrl=%2Fproducts *@
@* asp-page-handler: для Page Handlers *@
<a asp-page="./Details"
asp-page-handler="Download"
asp-route-id="@product.Id">
Завантажити
</a>
@* → /products/details?id=42&handler=Download *@
@* Головна сторінка *@
<a asp-page="/Index" class="navbar-brand">Головна</a>
Активне посилання у навігації
@* Виділяємо активний пункт меню *@
@{
var currentPage = ViewContext.RouteData.Values["page"]?.ToString();
}
<nav>
<a asp-page="/Products/Index"
class="nav-link @(currentPage?.StartsWith("/Products") == true ? "active" : "")">
Товари
</a>
</nav>
asp-validation-for і asp-validation-summary
Ці Tag Helpers показують повідомлення про помилки валідації, що додані у ModelState:
<form method="post">
<div class="mb-3">
<label asp-for="Input.Name" class="form-label">Назва</label>
<input asp-for="Input.Name" class="form-control">
@* Показує ModelState["Input.Name"] помилку *@
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Input.Price" class="form-label">Ціна</label>
<input asp-for="Input.Price" class="form-control">
<span asp-validation-for="Input.Price" class="text-danger"></span>
</div>
@* Зведення всіх помилок *@
@* ModelOnly: тільки загальні помилки (ModelState[string.Empty]) *@
@* All: всі помилки включаючи поля *@
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<button type="submit" class="btn btn-primary">Зберегти</button>
</form>
Для client-side validation — потрібно підключити jQuery Validate:
@section Scripts {
@* Стандартний partial з jQuery Validate + Unobtrusive *@
<partial name="_ValidationScriptsPartial" />
}
@* Стандартний файл шаблону — вже є у dotnet new webapp *@
<script src="~/lib/jquery-validation/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
Підключення цих скриптів активує автоматичну клієнтську валідацію: data-val-* атрибути (що генерує asp-for) обробляються jQuery Validate без жодного додаткового коду.
asp-items: select з варіантами
public class CreateModel : PageModel
{
[BindProperty]
public CreateInput Input { get; set; } = new();
// Список варіантів для вибору
public IEnumerable<SelectListItem> CategoryOptions { get; private set; } = [];
public async Task OnGetAsync()
{
var categories = await _svc.GetCategoriesAsync();
CategoryOptions = categories.Select(c => new SelectListItem
{
Value = c.Id.ToString(),
Text = c.Name
});
}
}
<select asp-for="Input.CategoryId"
asp-items="Model.CategoryOptions"
class="form-select">
<option value="">— Оберіть категорію —</option>
</select>
SelectListGroup та вкладені списки
CategoryOptions = categories
.GroupBy(c => c.ParentName ?? "Основні")
.SelectMany(g => g.Select(c => new SelectListItem
{
Value = c.Id.ToString(),
Text = c.Name,
Group = new SelectListGroup { Name = g.Key }
}));
Enum у select
// Автоматично з Enum
public IEnumerable<SelectListItem> StatusOptions =>
Enum.GetValues<ProductStatus>()
.Select(s => new SelectListItem
{
Value = s.ToString(),
Text = s.GetDisplayName() // Через DisplayAttribute або Humanizer
});
Form Tag Helper: asp-page, asp-antiforgery
@* Основна форма: POST на поточну сторінку *@
<form method="post">
@* AntiForgeryToken додається автоматично! *@
</form>
@* POST на конкретну сторінку *@
<form method="post" asp-page="./Create">
</form>
@* POST з page handler *@
<form method="post" asp-page-handler="Upload">
</form>
@* Без auto-generated AntiForgeryToken *@
<form method="post" asp-antiforgery="false">
@* Для AJAX форм де токен передається інакше *@
</form>
@* Enctype для файлів *@
<form method="post" enctype="multipart/form-data">
<input type="file" asp-for="Input.File">
</form>
Умовний контент: environment Tag Helper
@* Показати тільки у Development *@
<environment include="Development">
<div class="alert alert-warning">
⚠️ Development mode — не для Production!
</div>
<script src="~/js/debug-tools.js"></script>
</environment>
@* Показати тільки у Production *@
<environment exclude="Development">
<script src="https://cdn.example.com/app.min.js"
integrity="sha384-..."
crossorigin="anonymous">
</script>
</environment>
@* Production і Staging *@
<environment include="Production,Staging">
<!-- GTM, Cookie banner тощо -->
<script src="https://tagmanager.google.com/gtag.js"></script>
</environment>
cache Tag Helper: Output Cache у View
@* Кешуємо частину View на 60 секунд *@
<cache expires-after="@TimeSpan.FromSeconds(60)">
<div class="featured-products">
@* Цей блок рендериться рідко — рекламний банер, статистика *@
<h3>Популярні товари</h3>
@foreach (var p in await _svc.GetMostPopularAsync(5))
{
<p>@p.Name</p>
}
</div>
</cache>
@* Vary by: різний кеш для різних значень *@
<cache expires-after="@TimeSpan.FromMinutes(10)"
vary-by-user="true"
vary-by-query="category">
@* Унікальний кеш для кожного user + category *@
<partial name="_UserRecommendations" />
</cache>
@* Vary by route value *@
<cache expires-after="@TimeSpan.FromHours(1)"
vary-by-route="id">
@* Різний кеш для кожного /products/{id} *@
<partial name="_ProductSpecs" model="Model.Product" />
</cache>
@* Вимкнути кеш умовно *@
<cache enabled="@(!Model.IsAdminPreview)" expires-after="@TimeSpan.FromMinutes(30)">
@* Для звичайних користувачів — кешується, для адміна — завжди свіже *@
<partial name="_PriceList" />
</cache>
link і script: asp-append-version
@* apt-append-version: додає ?v=hash для cache busting *@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true">
@* → <link rel="stylesheet" href="/css/site.css?v=a3f9bc12d8"> *@
<script src="~/js/app.js" asp-append-version="true"></script>
@* → <script src="/js/app.js?v=f7e1a4d9"></script> *@
@* Хеш змінюється при зміні файлу → браузер завантажує нову версію *@
@* Але CDN може кешувати URL без ?v= — тоді потрібен filename hashing *@
Написання власного Tag Helper
Власний Tag Helper — C# клас що "обробляє" HTML-теги або атрибути:
using Microsoft.AspNetCore.Razor.TagHelpers;
// Обробляємо тег <alert> у .cshtml
[HtmlTargetElement("alert")]
public class AlertTagHelper : TagHelper
{
// Атрибути тега — публічні властивості
public string Type { get; set; } = "info"; // success, danger, warning, info
public string? Icon { get; set; }
public bool Dismissible { get; set; } = true;
public override async Task ProcessAsync(
TagHelperContext context,
TagHelperOutput output)
{
// Змінюємо тег <alert> на <div>
output.TagName = "div";
var iconHtml = Icon is not null ? $"<i class=\"bi bi-{Icon} me-2\"></i>" : "";
var dismissBtn = Dismissible
? "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\"></button>"
: "";
output.Attributes.SetAttribute("class",
$"alert alert-{Type} alert-dismissible fade show");
output.Attributes.SetAttribute("role", "alert");
// Отримуємо вміст тега
var content = await output.GetChildContentAsync();
output.Content.SetHtmlContent(
$"{iconHtml}{content.GetContent()}{dismissBtn}");
}
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyApp @* Власні Tag Helpers з нашої Assembly *@
@* Замість Bootstrap boilerplate: *@
<alert type="success" icon="check-circle" dismissible="true">
Товар успішно збережено!
</alert>
@* Більш складний приклад *@
<alert type="danger" dismissible="false">
<strong>Помилка!</strong> Неможливо видалити товар із активними замовленнями.
</alert>
Tag Helper для форми підтвердження видалення
// Атрибут-рівневий Tag Helper: додає підтвердження до будь-якої кнопки
[HtmlTargetElement("button", Attributes = "asp-confirm")]
public class ConfirmTagHelper : TagHelper
{
[HtmlAttributeName("asp-confirm")]
public string Message { get; set; } = "Ви впевнені?";
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.SetAttribute("onclick",
$"return confirm('{Message}')");
}
}
@* При кліку — питання "Підтвердити видалення?" *@
<button type="submit"
asp-confirm="Підтвердити видалення товару '{{Model.Product.Name}}'?"
class="btn btn-danger">
Видалити
</button>
Шпаргалка: всі основні Tag Helpers
| Tag Helper | HTML-елемент | Призначення |
|---|---|---|
asp-for | <input>, <textarea>, <select>, <label> | Зв'язування з властивістю |
asp-page | <a>, <form> | Посилання/форма на Razor Page |
asp-page-handler | <a>, <form> | Вказати Page Handler |
asp-route-{name} | <a>, <form> | Параметри маршруту/query |
asp-validation-for | <span> | Помилка валідації поля |
asp-validation-summary | <div> | Зведення помилок |
asp-items | <select> | Варіанти у select |
asp-antiforgery | <form> | AntiForgery token |
asp-append-version | <link>, <script> | Cache busting hash |
<environment include="..."> | Будь-який | Умовний вміст за середовищем |
<cache expires-after="..."> | Будь-який | Кешування блоку View |
<partial name="..." model="..."> | — | Підключення Partial View |
Практичні завдання
Рівень 1 — Базовий
Завдання 1.1. Реалізуйте власний <badge> Tag Helper з атрибутами type ("primary", "success", "danger") і text. Виводить <span class="badge bg-{type}">@text</span>. Використайте у картці товару для виводу кількості в запасі.
Завдання 1.2. Перегляньте Pages/Products/Create.cshtml і замініть всі ручні <input type="text" name="..."> на asp-for. Переконайтесь що валідація клієнтського боку спрацьовує (підключіть _ValidationScriptsPartial), без єдиного POST на сервер при порожній формі.
Рівень 2 — Логіка
Завдання 2.1. Напишіть <gravatar> Tag Helper: атрибут email і size (за замовчуванням 40). Генерує <img src="https://www.gravatar.com/avatar/{md5_of_email}?s={size}">. MD5 обчислюйте через MD5.HashData(). Використайте у _Layout.cshtml для аватара поточного користувача.
Завдання 2.2. Зробіть <sort-link> Tag Helper для сортованих таблиць: атрибути asp-page, column (назва колонки), current-sort і current-order. Генерує <a> з іконкою ▲/▼ якщо це активна колонка, і переключає order між asc/desc при кліку.
Razor синтаксис: шаблонізатор у .cshtml
Повний огляд Razor синтаксису у .cshtml файлах: @page, @model, @using, C# блоки @{ }, вирази @expression, керуючі конструкції @if/@foreach/@for/@switch, типізований @model, Layouts, @RenderBody/@RenderSection, секції Scripts, часткові подання (Partial Views), ViewComponent, HTML-кодування.
Форми і валідація: повний цикл обробки даних
Повний цикл форм у Razor Pages: HTML form з anti-forgery token, DataAnnotations для визначення правил, client-side validation через jQuery Validate Unobtrusive, server-side ModelState, IFormFile для завантаження файлів, обробка кількох кнопок submit, AJAX-форми через fetch API.