Razor Pages

Tag Helpers: типізований HTML

Детальний розгляд Tag Helpers у Razor Pages: asp-for, asp-page, asp-route-*, asp-validation-for, asp-validation-summary, asp-items, asp-append-version, environment, cache, link та script Tag Helpers, написання власного Tag Helper з TagHelper базового класу.

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". З магічними рядками — тихий баг.

Щоб Tag Helpers працювали, у _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 або безпосередньо
@section Scripts {
    @* Стандартний partial з jQuery Validate + Unobtrusive *@
    <partial name="_ValidationScriptsPartial" />
}
Pages/Shared/_ValidationScriptsPartial.cshtml
@* Стандартний файл шаблону — вже є у 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 з варіантами

PageModel
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
        });
    }
}
View
<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>

@* 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-теги або атрибути:

TagHelpers/AlertTagHelper.cs
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}");
    }
}
Реєстрація у _ViewImports.cshtml
@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 для форми підтвердження видалення

TagHelpers/DeleteButtonTagHelper.cs
// Атрибут-рівневий 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 HelperHTML-елементПризначення
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 при кліку.

Copyright © 2026