ASP.NET Core MVC

Display та Editor Templates

Display та Editor Templates в ASP.NET Core MVC: папки DisplayTemplates та EditorTemplates, Html.DisplayFor та Html.EditorFor, атрибут [UIHint], шаблони для складних типів (Money, DateRange, Address). Демо: ProductCard Display Template та MoneyEditor Editor Template.

Display та Editor Templates

Коли у вас у десяти різних View є поле типу decimal що відображає ціну — з символом валюти, правильним форматуванням та CSS-класом — ви хочете описати це відображення один раз і не копіювати код скрізь. Або маєте тип DateRange (діапазон дат) і кожна форма редагування потребує однакового джерела двох пов'язаних <input type="date">. Копіювати View-код — поганий варіант.

Display Templates та Editor Templates — механізм MVC що вирішує саме цю проблему. Ви описуєте як відображати тип (Display) та як редагувати тип (Editor) у .cshtml шаблонах, і ASP.NET Core автоматично застосовує їх через Html.DisplayFor() та Html.EditorFor().


Концепція: шаблон для типу, а не для сторінки

Ключова відмінність від Partial View: Display/Editor Templates прив'язані до типу C#, а не до імені файлу. Якщо ви створили MoneyEditor, то всюди де є Html.EditorFor(m => m.Price) і Price має тип Money — автоматично використовуватиметься ваш шаблон. Ніяких явних викликів.

Це схоже на перевизначення методу ToString() або operator ==, але для HTML-рендерингу.


Структура папок

Шаблони розміщуються у двох зарезервованих папках усередині Views/:

Views/
├── Shared/
│   ├── DisplayTemplates/          ← шаблони відображення (Html.DisplayFor)
│   │   ├── Money.cshtml           ← для типу Money
│   │   ├── DateRange.cshtml       ← для типу DateRange
│   │   └── Boolean.cshtml         ← перевизначення вбудованого типу bool
│   └── EditorTemplates/           ← шаблони редагування (Html.EditorFor)
│       ├── Money.cshtml           ← editor для Money
│       ├── DateRange.cshtml       ← editor для DateRange
│       └── EmailAddress.cshtml    ← для [DataType(DataType.EmailAddress)]

Ім'я файлу = ім'я типу (або ім'я DataType-атрибуту). Шаблони у Views/Shared/ діють глобально. Якщо потрібно перевизначити лише для конкретного Controller — помістіть у Views/{ControllerName}/DisplayTemplates/.


Html.DisplayFor() та Html.EditorFor() — як вони обирають шаблон

При виклику @Html.DisplayFor(m => m.Price) MVC шукає шаблон у такому порядку:

  1. Явно вказаний [UIHint("TemplateName")]DisplayTemplates/TemplateName.cshtml
  2. Ім'я типу властивості → DisplayTemplates/Money.cshtml (якщо Price має тип Money)
  3. [DataType(DataType.Currency)]DisplayTemplates/Currency.cshtml
  4. Вбудований шаблон для примітивних типів

Аналогічно для EditorFor. Пріоритет: явний [UIHint] > ім'я типу > DataType-атрибут > вбудований.


Демо-проєкт: ProductCard та MoneyEditor

Крок 1: Власний тип Money

Models/Money.cs
namespace ShopApp.Models;

// Value Object для грошової суми
public record Money(decimal Amount, string Currency = "UAH")
{
    // Форматування для відображення
    public string Formatted => Currency switch
    {
        "UAH" => $"{Amount:N2} ₴",
        "USD" => $"${Amount:N2}",
        "EUR" => $"€{Amount:N2}",
        _     => $"{Amount:N2} {Currency}"
    };

    public override string ToString() => Formatted;
}

Крок 2: Display Template для Money

Views/Shared/DisplayTemplates/Money.cshtml
@* Model — це безпосередньо об'єкт типу Money *@
@model ShopApp.Models.Money

@* Display Template — лише відображення, без форм *@
<span class="price @(Model.Amount == 0 ? "text-muted" : "text-success fw-bold")">
    @Model.Formatted
</span>

Крок 3: Editor Template для Money

Editor Template формує HTML-форму для редагування значення типу Money:

Views/Shared/EditorTemplates/Money.cshtml
@* Model — об'єкт Money, що редагується *@
@model ShopApp.Models.Money

@* ViewData["htmlFieldPrefix"] — містить правильний prefix для id/name *@
@{
    // Prefix для генерації правильних name-атрибутів (наприклад, "Price.Amount")
    var prefix = ViewData.TemplateInfo.HtmlFieldPrefix;
}

<div class="input-group">
    @* Поле суми *@
    <input type="number"
           id="@(prefix)_Amount"
           name="@(prefix).Amount"
           value="@Model.Amount.ToString("F2")"
           class="form-control"
           step="0.01"
           min="0"
           aria-label="Сума" />

    @* Dropdown валюти *@
    <select id="@(prefix)_Currency"
            name="@(prefix).Currency"
            class="form-select"
            style="max-width: 100px;"
            aria-label="Валюта">
        @foreach (var currency in new[] { "UAH", "USD", "EUR" })
        {
            <option value="@currency" selected="@(currency == Model.Currency)">
                @currency
            </option>
        }
    </select>
</div>
У Editor Templates важливо правильно генерувати name атрибути через ViewData.TemplateInfo.HtmlFieldPrefix. Саме цей prefix гарантує, що Model Binder правильно прив'яже Price.Amount та Price.Currency до вкладеного об'єкта Money у формі.

Крок 4: ViewModel з Money та DateRange

Models/ViewModels/ProductViewModel.cs
using System.ComponentModel.DataAnnotations;

namespace ShopApp.Models.ViewModels;

public class ProductViewModel
{
    public int Id { get; set; }

    [Required, StringLength(200)]
    public string Name { get; set; } = "";

    [Required]
    public string Description { get; set; } = "";

    // Тип Money → автоматично використає Money.cshtml
    public Money Price { get; set; } = new(0);
    public Money? OldPrice { get; set; }

    // UIHint — явно вказуємо шаблон
    [UIHint("StarRating")]
    public double Rating { get; set; }

    public bool InStock { get; set; }
    public string Category { get; set; } = "";

    // Дата у форматі для редагування
    [DataType(DataType.Date)]
    public DateTime CreatedAt { get; set; } = DateTime.Today;
}

Крок 5: Display Template StarRating (через [UIHint])

[UIHint("StarRating")] вказує що для цього поля треба взяти DisplayTemplates/StarRating.cshtml, ігноруючи ім'я типу (double):

Views/Shared/DisplayTemplates/StarRating.cshtml
@model double

@{
    // Перетворюємо рейтинг (0-10) на зірки (0-5)
    var stars = Math.Round(Model / 2, 1);
    var fullStars = (int)Math.Floor(stars);
    var hasHalf = stars - fullStars >= 0.5;
    var emptyStars = 5 - fullStars - (hasHalf ? 1 : 0);
}

<span class="star-rating" aria-label="Рейтинг @stars з 5 зірок" title="@Model:F1/10">
    @for (int i = 0; i < fullStars; i++)
    {
        <i class="bi bi-star-fill text-warning"></i>
    }
    @if (hasHalf)
    {
        <i class="bi bi-star-half text-warning"></i>
    }
    @for (int i = 0; i < emptyStars; i++)
    {
        <i class="bi bi-star text-muted"></i>
    }
    <small class="text-muted ms-1">(@Model:F1)</small>
</span>

Крок 6: Display Template ProductCard

Часто Display Template використовується для відображення складного об'єкта цілком — наприклад, картки продукту. Тоді Model у шаблоні — це вся ViewModel:

Views/Shared/DisplayTemplates/ProductCard.cshtml
@model ShopApp.Models.ViewModels.ProductViewModel

<div class="card h-100 shadow-sm">
    <div class="card-body">
        <div class="d-flex justify-content-between align-items-start">
            <h5 class="card-title mb-1">@Model.Name</h5>
            @if (!Model.InStock)
            {
                <span class="badge bg-secondary">Немає в наявності</span>
            }
        </div>

        <p class="card-text text-muted small mb-2">@Model.Category</p>

        @* Html.DisplayFor у Display Template — рекурсія! *@
        @* Рейтинг: шаблон StarRating.cshtml через [UIHint] *@
        <div class="mb-2">@Html.DisplayFor(m => m.Rating)</div>

        <p class="card-text">
            @(Model.Description.Length > 80
                ? Model.Description[..80] + "..."
                : Model.Description)
        </p>

        <div class="mt-auto">
            @* Ціна: шаблон Money.cshtml — автоматично за типом *@
            @Html.DisplayFor(m => m.Price)
            @if (Model.OldPrice is not null)
            {
                <small class="text-muted text-decoration-line-through ms-2">
                    @Html.DisplayFor(m => m.OldPrice)
                </small>
            }
        </div>
    </div>
    <div class="card-footer bg-transparent">
        <a asp-controller="Product" asp-action="Details" asp-route-id="@Model.Id"
           class="btn btn-sm btn-outline-primary">Детальніше</a>
    </div>
</div>

Крок 7: Використання у Views

Тепер Controller і Views залишаються чистими — вся складна логіка відображення у шаблонах:

Views/Product/Index.cshtml
@model IReadOnlyList<ShopApp.Models.ViewModels.ProductViewModel>

<h1>Каталог товарів</h1>

<div class="row row-cols-1 row-cols-md-3 g-4">
    @foreach (var product in Model)
    {
        <div class="col">
            @* DisplayFor з явним шаблоном ProductCard *@
            @Html.DisplayFor(_ => product, "ProductCard")

            @* Альтернатива через [UIHint("ProductCard")] на рівні типу *@
        </div>
    }
</div>
Views/Product/Edit.cshtml
@model ShopApp.Models.ViewModels.ProductViewModel
@{ ViewData["Title"] = "Редагувати товар"; }

<h1>Редагувати: @Model.Name</h1>

<form asp-action="Edit" method="post">
    <div class="mb-3">
        <label asp-for="Name" class="form-label">Назва</label>
        <input asp-for="Name" class="form-control">
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>

    <div class="mb-3">
        <label asp-for="Price" class="form-label">Ціна</label>
        @* EditorFor — автоматично використає EditorTemplates/Money.cshtml *@
        @Html.EditorFor(m => m.Price)
    </div>

    <div class="mb-3">
        <label asp-for="Rating" class="form-label">Рейтинг (з 10)</label>
        @* Звичайний EditorFor — без [UIHint], тому стандартний input для double *@
        <input asp-for="Rating" class="form-control" type="number" step="0.1" min="0" max="10">
    </div>

    <div class="mb-3">
        <div class="form-check">
            <input asp-for="InStock" class="form-check-input" type="checkbox">
            <label asp-for="InStock" class="form-check-label">В наявності</label>
        </div>
    </div>

    <div class="mb-3">
        <label asp-for="CreatedAt" class="form-label">Дата додавання</label>
        @* DataType.Date → EditorTemplates/Date.cshtml (вбудований) *@
        <input asp-for="CreatedAt" class="form-control" type="date">
    </div>

    <button type="submit" class="btn btn-primary">Зберегти</button>
    <a asp-action="Index" class="btn btn-outline-secondary ms-2">Скасувати</a>
</form>

@section Scripts {
    <partial name="_ValidationScriptsPartial"/>
}

Перевизначення вбудованих шаблонів

MVC має вбудовані шаблони для примітивних типів: String, Boolean, Decimal, DateTime тощо. Ви можете перевизначити їх, створивши файл з тим самим іменем:

Views/Shared/DisplayTemplates/Boolean.cshtml
@* Перевизначаємо відображення bool — замість "True"/"False" показуємо іконки *@
@model bool

@if (Model)
{
    <i class="bi bi-check-circle-fill text-success" title="Так"></i>
}
else
{
    <i class="bi bi-x-circle-fill text-danger" title="Ні"></i>
}

Тепер @Html.DisplayFor(m => m.IsPublished) скрізь у застосунку відображатиме іконки замість True/False.

Views/Shared/EditorTemplates/EmailAddress.cshtml
@* DataType.EmailAddress → EditorTemplates/EmailAddress.cshtml *@
@model string

<div class="input-group">
    <span class="input-group-text">
        <i class="bi bi-envelope"></i>
    </span>
    <input type="email"
           id="@ViewData.TemplateInfo.GetFullHtmlFieldId("")"
           name="@ViewData.TemplateInfo.GetFullHtmlFieldName("")"
           value="@Model"
           class="form-control"
           autocomplete="email"
           placeholder="example@mail.com">
</div>

Порівняння: коли що обирати

ПотребаРішення
Відображення складного об'єкта у одному місціPartial View (_ProductCard.cshtml)
Відображення типу скрізь де він зустрічаєтьсяDisplay Template (DisplayTemplates/Money.cshtml)
Однотипна форма редагування для типуEditor Template (EditorTemplates/Money.cshtml)
Перевизначення стандартних bool, DateTimeDisplay/Editor Template з іменем примітиву
Особливий шаблон лише для одного поля[UIHint("TemplateName")]
Незалежний блок з власною логікою з БДView Component

Практичні завдання

Рівень 1 — Базовий

Завдання 1.1. Створіть DisplayTemplates/DateTime.cshtml що відображає DateTime у форматі «dd MMMM yyyy о HH:mm» (наприклад, «27 березня 2026 о 14:30»). Переконайтеся що всі @Html.DisplayFor(m => m.CreatedAt) де CreatedAt є DateTime застосовують цей шаблон.

Завдання 1.2. Додайте DisplayTemplates/Boolean.cshtml що замість True/False виводить Bootstrap badge: <span class="badge bg-success">Так</span> або <span class="badge bg-secondary">Ні</span>. Застосуйте до поля Product.InStock.

Рівень 2 — Логіка

Завдання 2.1. Реалізуйте тип DateRange з властивостями Start та End (обидва DateTime). Створіть:

  • DisplayTemplates/DateRange.cshtml — відображає «від dd.MM.yyyy до dd.MM.yyyy», якщо End > Start, або «з dd.MM.yyyy» якщо End == default
  • EditorTemplates/DateRange.cshtml — два <input type="date"> що прив'язані до DateRange.Start та DateRange.End через правильні name-атрибути з prefix. Перевірте через форму що Model Binding правильно заповнює DateRange.

Завдання 2.2. Перевизначте EditorTemplates/String.cshtml — замість звичайного <input> відображайте <input> з лічильником символів <small>0 / 200</small> (оновлюється через oninput JavaScript). Переконайтеся що шаблон не ламає поля де [StringLength] не задано — використайте ViewData["StringLength"] або атрибут з ViewContext.ModelMetadata.ValidatorMetadata.

Рівень 3 — Архітектура

Завдання 3.1. Реалізуйте тип Address з полями Street, City, Country, PostalCode. Створіть:

  • DisplayTemplates/Address.cshtml — відображає адресу у вигляді форматованого блоку з <address> тегом
  • EditorTemplates/Address.cshtml — набір полів <input> з правильними name-атрибутами для кожного поля Address, включно з dropdown Country що містить список ["Україна", "Польща", "Німеччина", "США"]
  • Застосуйте до будь-якої ViewModel що має властивість Address DeliveryAddress. Переконайтеся що Model Binding після submit форми повертає коректно заповнений об'єкт Address

Резюме

  • Display Templates (Views/Shared/DisplayTemplates/) — визначають як відображати тип. Викликаються через Html.DisplayFor()
  • Editor Templates (Views/Shared/EditorTemplates/) — визначають як редагувати тип у формі. Викликаються через Html.EditorFor()
  • Шаблон обирається автоматично за іменем типу (назва .cshtml = назва C#-типу). Вручну — через [UIHint("TemplateName")]
  • Тип Model у шаблоні — безпосередньо об'єкт відповідного типу
  • У Editor Templates генерувати name-атрибути через ViewData.TemplateInfo.HtmlFieldPrefix для коректного Model Binding
  • Вбудовані шаблони (Boolean, String, DateTime тощо) можна перевизначити — файл з тим самим іменем у DisplayTemplates/ або EditorTemplates/
  • [DataType(DataType.EmailAddress)]EmailAddress.cshtml; [UIHint("StarRating")]StarRating.cshtml

У наступній статті — Validation Advanced: IValidatableObject для крос-field валідації, Remote validation, власні ValidationAttribute та інтеграція FluentValidation з MVC.