Коли у вас у десяти різних 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 шукає шаблон у такому порядку:
[UIHint("TemplateName")] → DisplayTemplates/TemplateName.cshtmlDisplayTemplates/Money.cshtml (якщо Price має тип Money)[DataType(DataType.Currency)] → DisplayTemplates/Currency.cshtmlАналогічно для EditorFor. Пріоритет: явний [UIHint] > ім'я типу > DataType-атрибут > вбудований.
Moneynamespace 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;
}
Money@* Model — це безпосередньо об'єкт типу Money *@
@model ShopApp.Models.Money
@* Display Template — лише відображення, без форм *@
<span class="price @(Model.Amount == 0 ? "text-muted" : "text-success fw-bold")">
@Model.Formatted
</span>
MoneyEditor Template формує HTML-форму для редагування значення типу Money:
@* 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>
name атрибути через ViewData.TemplateInfo.HtmlFieldPrefix. Саме цей prefix гарантує, що Model Binder правильно прив'яже Price.Amount та Price.Currency до вкладеного об'єкта Money у формі.Money та DateRangeusing 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;
}
StarRating (через [UIHint])[UIHint("StarRating")] вказує що для цього поля треба взяти DisplayTemplates/StarRating.cshtml, ігноруючи ім'я типу (double):
@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>
ProductCardЧасто Display Template використовується для відображення складного об'єкта цілком — наприклад, картки продукту. Тоді Model у шаблоні — це вся ViewModel:
@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>
Тепер Controller і Views залишаються чистими — вся складна логіка відображення у шаблонах:
@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>
@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 тощо. Ви можете перевизначити їх, створивши файл з тим самим іменем:
@* Перевизначаємо відображення 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.
@* 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, DateTime | Display/Editor Template з іменем примітиву |
| Особливий шаблон лише для одного поля | [UIHint("TemplateName")] |
| Незалежний блок з власною логікою з БД | View Component |
Завдання 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.1. Реалізуйте тип DateRange з властивостями Start та End (обидва DateTime). Створіть:
DisplayTemplates/DateRange.cshtml — відображає «від dd.MM.yyyy до dd.MM.yyyy», якщо End > Start, або «з dd.MM.yyyy» якщо End == defaultEditorTemplates/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.1. Реалізуйте тип Address з полями Street, City, Country, PostalCode. Створіть:
DisplayTemplates/Address.cshtml — відображає адресу у вигляді форматованого блоку з <address> тегомEditorTemplates/Address.cshtml — набір полів <input> з правильними name-атрибутами для кожного поля Address, включно з dropdown Country що містить список ["Україна", "Польща", "Німеччина", "США"]Address DeliveryAddress. Переконайтеся що Model Binding після submit форми повертає коректно заповнений об'єкт AddressViews/Shared/DisplayTemplates/) — визначають як відображати тип. Викликаються через Html.DisplayFor()Views/Shared/EditorTemplates/) — визначають як редагувати тип у формі. Викликаються через Html.EditorFor().cshtml = назва C#-типу). Вручну — через [UIHint("TemplateName")]Model у шаблоні — безпосередньо об'єкт відповідного типуname-атрибути через ViewData.TemplateInfo.HtmlFieldPrefix для коректного Model BindingBoolean, String, DateTime тощо) можна перевизначити — файл з тим самим іменем у DisplayTemplates/ або EditorTemplates/[DataType(DataType.EmailAddress)] → EmailAddress.cshtml; [UIHint("StarRating")] → StarRating.cshtmlУ наступній статті — Validation Advanced: IValidatableObject для крос-field валідації, Remote validation, власні ValidationAttribute та інтеграція FluentValidation з MVC.
View Components: повторювані незалежні блоки UI
View Components в ASP.NET Core MVC: базовий клас ViewComponent, метод InvokeAsync, структура папок Views/Shared/Components. Порівняння з Partial Views та Tag Helpers. DI у View Component. Демо: ShoppingCartViewComponent, NotificationBellViewComponent, BreadcrumbViewComponent.
Валідація: IValidatableObject та FluentValidation
Просунута валідація в ASP.NET Core MVC: IValidatableObject для cross-field правил, Remote validation через [Remote], власні ValidationAttribute, інтеграція FluentValidation з MVC. Демо: форма реєстрації з [Remote] та FluentValidation для складних бізнес-правил замовлення.