Razor Pages

Razor синтаксис: шаблонізатор у .cshtml

Повний огляд Razor синтаксису у .cshtml файлах: @page, @model, @using, C# блоки @{ }, вирази @expression, керуючі конструкції @if/@foreach/@for/@switch, типізований @model, Layouts, @RenderBody/@RenderSection, секції Scripts, часткові подання (Partial Views), ViewComponent, HTML-кодування.

Razor синтаксис: шаблонізатор у .cshtml

Razor — це шаблонізатор, що поєднує C# і HTML в одному файлі. Символ @ — перемикач між HTML-режимом і C#-режимом. Все що після @ — це C#, все інше — HTML.

Razor не є унікальним для Razor Pages: він використовується і в MVC Views, і в Blazor. Але для нас зараз важливий контекст Razor Pages.


Директиви: @page, @model, @using

Директиви — це спеціальні Razor-інструкції що починаються з @ і впливають на поведінку всього файлу:

Pages/Products/Index.cshtml
@* ─── Обов'язкові директиви ─────────────────────────────── *@

@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 &amp; Tea</h1>  ← & замінено на &amp; *@

@* Метод чи властивість *@
<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().

Pages/Shared/_Layout.cshtml
<!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-бібліотеку тільки на сторінках де вона потрібна:

Pages/Products/Create.cshtml
@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

Pages/_ViewStart.cshtml
@{
    // Layout за замовчуванням для всіх сторінок
    Layout = "_Layout";
}
Pages/Admin/Products/Index.cshtml
@page
@model IndexModel
@{
    // Перевизначаємо Layout для адмін-сторінок
    Layout = "_AdminLayout";
    ViewData["Title"] = "Продукти (адмін)";
}
Pages/Errors/404.cshtml
@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.

Pages/Shared/_ProductCard.cshtml
@* 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

Сторінка що використовує Partial
@* ─── Спосіб 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 шукаються у:

  1. Поточна папка (Pages/Products/_ProductCard.cshtml)
  2. 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>&lt;script&gt;alert('xss')&lt;/script&gt;</p> *@

@* Небезпечно: Html.Raw обходить кодування *@
@Html.Raw(Model.UserName)
@* Виводить: <p><script>alert('xss')</script></p>  ← XSS! *@

@* Html.Raw використовуйте ТІЛЬКИ для:                  *@
@* 1. HTML що ви самі згенерували (не від користувача)  *@
@* 2. Після явної санітизації через HtmlSanitizer       *@

Правило: ніколи не використовуйте Html.Raw для даних що прийшли від користувача.


@inject: інжекція у View

Pages/Products/Index.cshtml
@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).

Copyright © 2026