Razor синтаксис: шаблонізатор у .cshtml
Razor синтаксис: шаблонізатор у .cshtml
Razor — це шаблонізатор, що поєднує C# і HTML в одному файлі. Символ @ — перемикач між HTML-режимом і C#-режимом. Все що після @ — це C#, все інше — HTML.
Razor не є унікальним для Razor Pages: він використовується і в MVC Views, і в Blazor. Але для нас зараз важливий контекст Razor Pages.
Директиви: @page, @model, @using
Директиви — це спеціальні Razor-інструкції що починаються з @ і впливають на поведінку всього файлу:
@* ─── Обов'язкові директиви ─────────────────────────────── *@
@page
@* Обов'язкова директива для Razor Pages.
Без неї файл — звичайний MVC View, не Razor Page.
@page може містити шаблон маршруту: *@
@page "{id:int}"
@* → /products/42 *@
@page "{id:int?}"
@* → /products або /products/42 *@
@model IndexModel
@* Вказує тип PageModel для цієї сторінки.
Model.Products, Model.Search — доступ через Model.*@
@* ─── Додаткові директиви ───────────────────────────────── *@
@using MyApp.Models
@* Те саме що using у C# файлі.
Зазвичай краще додавати у _ViewImports.cshtml *@
@using static MyApp.Helpers.FormatHelper
@* Статичні методи — для зручного доступу у шаблоні *@
@inject IStringLocalizer<SharedResource> T
@* Ін'єкція сервісів прямо у .cshtml
Доступний як T["WelcomeMessage"] *@
@section Scripts {
@* Секція для JavaScript — додається у конкретне місце Layout *@
<script>console.log("Page loaded");</script>
}
@{
ViewData["Title"] = "Список товарів";
Layout = "_Layout"; // Зазвичай з _ViewStart, але можна перевизначити
}
C# блоки: @{ } і вирази @expression
Вирази виводу
@* Просте значення — автоматично HTML-encode *@
<h1>@Model.ProductName</h1>
@* Виводить: <h1>Coffee & Tea</h1> ← & замінено на & *@
@* Метод чи властивість *@
<p>Дата: @DateTime.Now.ToString("dd.MM.yyyy")</p>
<p>Ціна: @Model.Price.ToString("C", new CultureInfo("uk-UA")) ₴</p>
@* Тернарний оператор — у круглих дужках *@
<span class="badge @(Model.InStock ? "bg-success" : "bg-danger")">
@(Model.InStock ? "В наявності" : "Немає")
</span>
@* Null-conditional і coalescing *@
<p>@(Model.Description ?? "Опис відсутній")</p>
<p>@(Model.Author?.Name ?? "Анонім")</p>
@* Екранування @ — подвійний @@ *@
<p>Email: user@@example.com</p>
@* Html.Raw: вивід без HTML-encode (обережно з XSS!) *@
@Html.Raw("<strong>Bold</strong>")
@* ← Використовуйте ТІЛЬКИ для довіреного HTML *@
Блоки коду @{ }
@{
// Повноцінний C# код
var title = "Список товарів";
ViewData["Title"] = title;
// Можна оголошувати змінні
var discountedProducts = Model.Products
.Where(p => p.Discount > 0)
.OrderByDescending(p => p.Discount)
.ToList();
// Умовне визначення класу
var heroClass = Model.Products.Count == 0 ? "text-muted" : "text-dark";
}
<h1 class="@heroClass">@title</h1>
<p>Зі знижкою: @discountedProducts.Count товарів</p>
Керуючі конструкції
@if / @else if / @else
@if (Model.Products.Count == 0)
{
<div class="alert alert-warning">
<p>Товарів не знайдено.</p>
<a asp-page="./Create" class="btn btn-primary">Додати перший товар</a>
</div>
}
else if (Model.Products.Count < 5)
{
<div class="alert alert-info">
<p>Небагато товарів: @Model.Products.Count штук.</p>
</div>
<!-- Решта вмісту... -->
}
else
{
<p class="text-muted">Знайдено: @Model.Products.Count товарів</p>
}
@foreach
<div class="row">
@foreach (var product in Model.Products)
{
<div class="col-md-4 mb-3">
<div class="card @(product.InStock ? "" : "opacity-50")">
<div class="card-body">
<h5 class="card-title">@product.Name</h5>
<p class="card-text">@product.Price ₴</p>
@if (product.Discount > 0)
{
<span class="badge bg-danger">-@product.Discount%</span>
}
<a asp-page="./Details"
asp-route-id="@product.Id"
class="btn btn-outline-primary btn-sm">
Деталі
</a>
</div>
</div>
</div>
}
</div>
@for, @while
@* Пагінація через @for *@
<nav>
<ul class="pagination">
@for (int i = 1; i <= Model.TotalPages; i++)
{
<li class="page-item @(i == Model.Page ? "active" : "")">
<a class="page-link"
asp-page="./Index"
asp-route-page="@i"
asp-route-search="@Model.Search">
@i
</a>
</li>
}
</ul>
</nav>
@switch
@switch (Model.Product.Status)
{
case ProductStatus.Active:
<span class="badge bg-success">Активний</span>
break;
case ProductStatus.Draft:
<span class="badge bg-secondary">Чернетка</span>
break;
case ProductStatus.Archived:
<span class="badge bg-dark">Архів</span>
break;
default:
<span class="badge bg-light text-dark">Невідомо</span>
break;
}
Layouts: спільний HTML-каркас
Layout — шаблон що огортає всі сторінки. Думайте про нього як про "обгортку": header, footer, navigation — один раз у Layout, вміст кожної сторінки — через @RenderBody().
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@* ViewData["Title"] задається у кожній сторінці *@
<title>@ViewData["Title"] — MyShop</title>
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true">
@* asp-append-version: додає ?v=hash для cache busting *@
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" asp-page="/Index">
<i class="bi bi-shop"></i> MyShop
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" asp-page="/Products/Index">Товари</a>
<a class="nav-link" asp-page="/Categories/Index">Категорії</a>
</div>
</div>
</nav>
<main class="container py-4">
@* Тут вставляється вміст кожної сторінки *@
@RenderBody()
</main>
<footer class="bg-light py-3 mt-5">
<div class="container text-center text-muted">
© @DateTime.Now.Year MyShop
</div>
</footer>
<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@* Секція для скриптів конкретних сторінок *@
@await RenderSectionAsync("Scripts", required: false)
@* required: false — секція необов'язкова *@
</body>
</html>
Секції (@section)
Секції дозволяють сторінці вставляти вміст у конкретне місце Layout — наприклад, підключити JS-бібліотеку тільки на сторінках де вона потрібна:
@page
@model CreateModel
@{
ViewData["Title"] = "Новий товар";
}
<h1>Новий товар</h1>
<form method="post">
<input asp-for="Input.Name">
<!-- форма -->
</form>
@* Секція Scripts — вставляється в Layout де @RenderSectionAsync("Scripts") *@
@section Scripts {
@* Скрипти клієнтської валідації (стандартний файл) *@
<partial name="_ValidationScriptsPartial" />
@* Власні скрипти тільки для цієї сторінки *@
<script>
document.getElementById('Input_Name').addEventListener('input', function() {
// preview logic
});
</script>
}
Кілька Layout
@{
// Layout за замовчуванням для всіх сторінок
Layout = "_Layout";
}
@page
@model IndexModel
@{
// Перевизначаємо Layout для адмін-сторінок
Layout = "_AdminLayout";
ViewData["Title"] = "Продукти (адмін)";
}
@page
@model ErrorModel
@{
// Сторінка помилки без навігації
Layout = null;
}
<!DOCTYPE html>
<html>
<body>
<h1>404 — Не знайдено</h1>
</body>
</html>
Partial Views: перевикористовувані шаблони
Partial View — частина HTML що можна вбудовувати у кілька сторінок. Аналогія: якщо у Minimal API ви виносили логіку у метод — у Razor Pages виносите HTML у Partial.
@* Partial View: отримує Product як @model *@
@model Product
<div class="card h-100 shadow-sm">
@if (Model.ImageUrl is not null)
{
<img src="@Model.ImageUrl" class="card-img-top" alt="@Model.Name">
}
<div class="card-body">
<h5 class="card-title">@Model.Name</h5>
<p class="card-text text-muted">@Model.Description</p>
<div class="d-flex justify-content-between align-items-center">
<span class="fs-5 fw-bold">@Model.Price.ToString("N0") ₴</span>
@if (Model.Discount > 0)
{
<span class="badge bg-danger">-@Model.Discount%</span>
}
</div>
</div>
<div class="card-footer">
<a asp-page="/Products/Details"
asp-route-id="@Model.Id"
class="btn btn-primary btn-sm w-100">
Детальніше
</a>
</div>
</div>
Способи підключення Partial View
@* ─── Спосіб 1: <partial> Tag Helper (рекомендовано) *@
<div class="row">
@foreach (var product in Model.Products)
{
<div class="col-md-4 mb-3">
@* model: передає конкретний об'єкт у partial *@
<partial name="_ProductCard" model="product" />
</div>
}
</div>
@* ─── Спосіб 2: await Html.PartialAsync (для складних випадків) *@
<partial name="_ProductCard" model="Model.FeaturedProduct" />
@await Html.PartialAsync("_ProductCard", Model.FeaturedProduct)
@* ─── Спосіб 3: ViewData у Partial *@
<partial name="_Alert" view-data='@(new ViewDataDictionary(ViewData)
{ { "AlertType", "success" }, { "Message", "Збережено!" } })' />
Partial у Shared або поряд
Partial Views шукаються у:
- Поточна папка (
Pages/Products/_ProductCard.cshtml) Pages/Shared/(Pages/Shared/_ProductCard.cshtml)
Pages/
├── Shared/
│ ├── _Layout.cshtml
│ ├── _ProductCard.cshtml ← Доступна з будь-якої сторінки
│ └── _Alert.cshtml
└── Products/
├── _ProductDetails.cshtml ← Доступна тільки з Products/
├── Index.cshtml
└── Details.cshtml
Коментарі у Razor
@* Razor коментар: НЕ потрапляє у HTML *@
<!-- HTML коментар: потрапляє у HTML (видно у DevTools) -->
@{
// C# коментар у блоку
/* Багаторядковий C# коментар */
}
HTML-кодування: захист від XSS
Razor автоматично HTML-кодує всі @expression. Це захист від XSS:
// Якщо user.Name = "<script>alert('xss')</script>"
@* Безпечно: Razor кодує автоматично *@
<p>@Model.UserName</p>
@* Виводить: <p><script>alert('xss')</script></p> *@
@* Небезпечно: Html.Raw обходить кодування *@
@Html.Raw(Model.UserName)
@* Виводить: <p><script>alert('xss')</script></p> ← XSS! *@
@* Html.Raw використовуйте ТІЛЬКИ для: *@
@* 1. HTML що ви самі згенерували (не від користувача) *@
@* 2. Після явної санітизації через HtmlSanitizer *@
Правило: ніколи не використовуйте Html.Raw для даних що прийшли від користувача.
@inject: інжекція у View
@page
@model IndexModel
@* Ін'єкція сервісів прямо у .cshtml — без PageModel *@
@inject IStringLocalizer<ProductsResource> L
@inject PricingService Pricing
<h1>@L["ProductsTitle"]</h1>
@foreach (var p in Model.Products)
{
@* Сервіс доступний як змінна *@
var finalPrice = Pricing.CalculateWithDiscount(p.Price, p.Discount);
<p>@p.Name: @finalPrice ₴</p>
}
@inject корисний для "View-рівневої" логіки: форматування, локалізація, невеликі обчислення. Для бізнес-логіки — тримайте в PageModel.Практичні завдання
Рівень 1 — Базовий
Завдання 1.1. Створіть Pages/Shared/_ProductCard.cshtml зі стилізованою картою товару (Bootstrap card). Виведіть у ній: зображення (якщо є), назву, ціну, знижку (badge якщо Discount > 0), та кнопку "Деталі". Використайте Partial у Pages/Products/Index.cshtml через <partial name="_ProductCard" model="product">.
Завдання 1.2. У Pages/_Layout.cshtml додайте @section з назвою "Breadcrumbs". У Pages/Products/Details.cshtml — виводьте через @section Breadcrumbs { } навігаційний breadcrumb: Головна / Товари / @Model.Product.Name.
Рівень 2 — Логіка
Завдання 2.1. Реалізуйте Pages/Shared/_Pagination.cshtml — partial з пагінацією. Прийміть через ViewData: CurrentPage, TotalPages, Search, та PageRouteValues (словник для asp-route-*). Partial виводить кнопки 1..TotalPages. Використайте у Products/Index і Categories/Index.
Завдання 2.2. Додайте @inject IMemoryCache Cache у Pages/Products/Index.cshtml. Виводте в правому куті сторінки <small class="text-muted">Кеш: X записів</small> де X — кількість записів у кеші (якщо MemoryCache.GetCurrentStatistics() не null).
PageModel: логіка сторінки Razor Pages
Детальний розгляд PageModel в Razor Pages: OnGet/OnPost/OnPutAsync handler-методи, [BindProperty] і [BindProperty(SupportsGet)], ModelState і повідомлення валідації, return Page/RedirectToPage/NotFound/Content, TempData vs ViewData vs властивості PageModel, Page Handlers для кількох форм.
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 базового класу.