Если вы, будучи разработчиком ASP.NET Web Forms, сомневаетесь в переносимости своих навыков на более современную платформу .NET (например, .NET Core или .NET 6), то спешу вас успокоить — все не так уж и плохо. Хоть Microsoft и не планирует переносить Web Forms, приобретенные вами навыки вполне применимы в аналогичном фреймворке под названием Razor Pages. Да, вам все равно придется адаптировать свое мышление к этой новой платформе, но если вы не пожалеете на это время, ты вы откроете для себя такой же мощный и простой в использовании инструмент для создания веб-приложений.
Откуда мы начинали свой путь
В начале 2000-х годов я был одним из тех C++ разработчиков, которых можно было бы охарактеризовать фразой: "Вам придется вырывать указатели из моих холодных мертвых рук". Но как только я узнал, как работает сборка мусора в .NET, я был обращен в новую религию. В те ранние дни я писал на ASP.NET (придя к это через разработку компонентов для ASP-проектов).
И хоть я не мог похвастаться пониманием того, как на самом деле устроен веб, мне было поручено разрабатывать с помощью ASP.NET веб-сайты и веб-приложения. На помощь пришел Microsoft со своими Web Forms. Сегодня разные лагери разработчиков дружно критикуют Web Forms за то, насколько эта платформа далека от традиционной веб-разработки. Но она помогла таким людям, как я, окунуться в веб-мир без сковывающего страха, который возникает перед чем-то неизведанным. Microsoft успешно превратила разработчиков настольных приложений в веб-разработчиков. Но эта инициатива не была лишена неминуемых рисков.
Web Forms привнесла в веб-разработку drag-n-drop проектирование. Все тонкости работы с вебом были по возможности упрятаны под капот, а написание серверного кода было максимально приближено к разработке stateful-решений. Добавьте сюда ViewState и Session State, и многие разработчики смогли принести много пользы для своих компаний и работодателей.
Но сейчас на дворе 2024 год. С тех пор мы стали свидетелями множества перемен. Задача изучения JavaScript для клиента, разделения ответственности на контроллеры и представления, а также написания настоящего stateless-кода для многих разработчиков веб-форм может оказаться непосильной. Но именно на этой части пути мы сейчас и находимся. Идеального решения для перехода на ASP.NET Core для разработчиков веб-форм не существует. Но способы применить имеющиеся знания, не “выплескивая вместе с водой ребенка”, все же есть. На выручку приходит Razor Pages.
Хотя в Razor Pages у вас нет WYSIWYG-редактора, вы можете использовать свои навыки разработки с фокусом на страницы, полученные в Web Forms, для успешной работы в ASP.NET Core.
Представляем Razor Pages
В качестве ответа на Web Pages компания Microsoft представила ASP.NET MVC — Model-View-Controller фреймворк, который разделял представления и логику (и упрощал тестирование). И хоть это был преобладающий во многих проектах фреймворк, он так и не заменил Web Forms. После появления .NET Core появился Razor Pages, который вместо полного разделения рассматривал модель больше как постраничное решение. Теперь, с появлением Blazor, в нашем арсенале инструментов появилось еще одно решение. В этой статье я сосредоточусь на Razor Pages, поскольку считаю, что это наиболее простой путь перехода для разработчиков веб-форм.
Именование — сложнейшая задача в разработке программного обеспечения
Прежде чем мы начнем, я хочу определить некоторые термины. Razor — это язык для добавления логики в HTML-разметку (он был разработан для ASP.NET MVC). Поскольку этот язык невероятно полезен, он используется во многих технологиях Microsoft-стека, из-за чего мы оказались в ситуации, когда практически все стало называться Razor-что-то. В большинстве случаев файлы Razor заканчиваются расширениями ".cshtml" или ".vbhtml". Итак, давайте попробуем разобраться:
Razor View: Файлы, связанные с представлением (View) в ASP.NET MVC
Razor Page: Файлы, связанные со страницей (Page) в Razor Pages
Razor Component: Компонент, используемый фреймворком Blazor для веб-приложений на основе Web Assembly.
Оговорив это, мы сосредоточим наше внимание именно на работе Razor Pages.
Давайте попробуем сопоставить номенклатуру Web Forms с номенклатурой Razor Pages, как показано в таблице 1.
Таблица 1: Перевод терминов WebForms в Razor Pages
WebForms Term | Razor Page Term |
Web Form (.aspx) | Razor Page (.cshtml/vbhtml) |
Web Control (.ascx) | Partial Page (also .cshtml/vbhtml) Partial Page (также .cshtml/vbhtml) |
MasterPage | Layout |
AJAX | Просто JavaScript |
global.asax | Program.cs или Startup.cs |
Краткий обзор
В основе Razor Pages лежат две довольно простые концепции:
Соглашение для структуры URL
Представление контента в форме Razor-страниц
Имеется в виду, что при создании нового проекта с помощью Razor Pages для обработки запросов добавляется новая middleware-составляющая:
var app = builder.Build();
// Настройка конвейера HTTP-запросов.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
MapRazorPages просто прослушивает запросы и смотрит, есть ли совпадение с файлами Razor Page. Если оно найдено, middleware возвращает сгенерированную страницу, как показано на рисунке 1.
Как она узнает, существует ли страница Razor Page для данного запроса? Она использует соглашение для поиска файлов. Хотя конкретная реализация может немного отличаться, вы можете представить папку Pages как корень веб-сервера. Это означает, что для ответа на запросы Razor Pages будет следовать структуре папка/файл. Например, если URL запроса — /contact, middleware будет искать в папке Pages файл с именем contact.cshtml. Если он будет найден, то она сгенерирует эту страницу и вернет ее клиенту, как показано на рисунке 2.
Исключением является файл Index.cshtml. Этот файл используется, когда URL указывает непосредственно на папку, а не на имя отдельного файла. Это имя страницы по умолчанию и запасной вариант. Это означает, что если URL выглядит как https://localhost/, он будет искать index.cshtml.
Папки работают аналогичным образом. Любые папки внутри папки Pages структурно соответствуют фрагментам URL. Например, если у вас есть URL типа /Sales/ProductList, соответствующий ему файл будет выглядеть, как показано на рисунке 3.
Итак, теперь, когда вы видите, как отображаются Razor-страницы, давайте разберемся, из чего они состоят.
Анатомия Razor Page
Хоть в большинстве случаев вы будете использовать для создания Razor-страниц существующие заготовки, давайте рассмотрим, что делает Razor-страницу Razor-страницей. Razor Page — это просто файл, содержащий объявление @page:
@page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Hello from Razor</h1>
</body>
</html>
Таким образом мы сигнализируем middleware, что это обслуживаемый файл. Папка Pages может содержать другие файлы, например, файлы макета (layout) или частичных (partial) представлений, которые вы не хотите отображать как отдельные страницы. Это помогает middleware определить, действительно ли это Razor Page или нет. Знак "@" не является случайностью. В синтаксисе Razor символ "@" используется для обозначения начала операции серверного кода. Например, вы можете создать произвольный блок кода следующим образом:
@page
@{
var title = "This is made with Razor Pages";
}
<!DOCTYPE html>
Фигурные скобки (т.е. {}) нужны только для того, чтобы сделать код многострочным. Этот код — обычный C# (или VB.NET, если вы используете файлы .vbhtml). Но опять же, начинать его нужно с символа "@":
@page
@{
var title = "CODE Magazine - Razor Pages";
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/css/site.css" rel="stylesheet" />
<title>@title</title>
</head>
<body>
<div>
<h1>@title</h1>
</div>
</body>
</html>
With a variable created here, you can just insert the title in both places you need it. Razor also allows you to call methods or properties because everything after the @ sign is interpreted as the language of the file:
Создав таким образом переменную (title), вы можете просто вставить ее куда вам нужно. Razor также позволяет вызывать методы или свойства, поскольку все, что следует за знаком @, интерпретируется в рамках языка файла:
<p>Today is @DateTime.Now.ToShortDateString()</p>
Вы даже можете использовать операторы управления потоком выполнения:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/css/site.css" rel="stylesheet" />
<title>@title</title>
</head>
<body>
<div>
@if (string.IsNullOrEmpty(@title) == false)
{
<h1>@title</h1>
}
</div>
</body>
</html>
Чтобы определить, показывать ли title, мы используем оператор If. Точно так же будет работать и остальные управляющие операторы, такие как for, foreach, switch и т. д.
Если вы пришли из Web Forms, то подобный серверный синтаксис должен быть вам удобен, хоть он и отличается. Но меня беспокоит то, что использование встроенного кода таким образом может смешать логику и дизайн. Вот тут-то и приходят на помощь модели страниц.
Класс PageModel
Когда вы создаете новую Razor-страницу с помощью Visual Studio, она автоматически добавляет одноименный .cshtml.cs-файл. Он называется классом PageModel. Например, индексная страница будет выглядеть следующим образом:
// Index.cshtml.cs
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace CodeRazorPages.Pages;
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
Этот класс наследуется от класса PageModel. Этот класс привязывается к cshtml-файлу с помощью объявления @model на странице соответствующей странице:
@page
@model IndexModel
Сделав это, вы получите доступ к инстансу PageModel. Если вы хотите добавить свойство в класс PageModel, вы можете сделать это следующим образом:
// Index.cshtml.cs
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace CodeRazorPages.Pages;
public class IndexModel : PageModel
{
public string Title { get; set; } = "Welcome";
public void OnGet()
{
}
}
Затем вы можете использовать его в cshtml-файле, как показано здесь:
@page
@model IndexModel
...
<h1>@Model.Title</h1>
<p>Today is @DateTime.Now.ToShortDateString()</p>
...
Объявление @model дает нам доступ к свойству Model, которая является инстансом класса IndexModel. Как видите, с помощью синтаксиса Razor можно внедрить на страницу Title, который вы определили в классе PageModel.
С классом PageModel можно реализовывать и более сложные взаимодействия. Вы могли заметить, что класс PageModel содержит метод OnGet. Этот метод выполняется перед отрисовкой страницы, если она была получена с помощью метода GET. Таким образом, в нашем примере можно сделать что-то вроде этого:
public class IndexModel : PageModel
{
public string Title { get; set; } = "Welcome";
public List<double> InvoiceTotals { get; set; } = new List<double>();
public void OnGet()
{
for (var x = 0; x < 10; ++x)
{
InvoiceTotals.Add(Random.Shared.NextDouble() * 100);
}
}
}
Таким образом мы можем создавать контент (в данном случае случайные итоговые суммы), а затем использовать его:
<h1>Model.Title</h1>
<p>Today is @DateTime.Now.ToShortDateString()</p>
<h3>Invoices</h3>
@foreach (var invoice in Model.InvoiceTotals)
{
<div>$ @invoice.ToString("0.00")</div>
}
Хоть это и достаточно сложный пример, но вы можете представить себе сценарий, где мы считываем что-либо из базы данных, чтобы отобразить это точно таким же образом. Мы бы хотели делать это в методе OnGet, а не в конструкторе, потому что мы бы не хотели генерировать данные, если на эту страницу не GET-, а POST-запрос, что мы увидим дальше.
Привязка к модели
При создании форм (или других взаимодействиях) в Razor Pages вы можете использовать привязку к модели (model binding) — это позволит получить данные из формы и внедрить их в класс PageModel. Сейчас я все объясню. Начнем с простого класса, в котором хранятся данные о пользователе:
public class UserSettings
{
public string? UserId { get; set; }
public string? FullName { get; set; }
}
Добавим свойство для UserSettings в классе PageModel:
public UserSettings Settings { get; set; } = new UserSettings();
public void OnGet()
{
Settings.UserId = "shawn@aol.com";
Settings.FullName = "Shawn Wildermuth";
}
Затем создадим форму на этой странице. Обратите внимание, что Settings доступны из Model:
<h1>Settings</h1>
<form method="POST">
<label>Email</label>
<input type="email" name="UserId" value="@Model.Settings.UserId" />
<label>Full Name</label>
<input type="text" name="FullName" value="@Model.Settings.FullName" />
<button type="submit">Send...</button>
</form>
Также обратите внимание, что указанное имя соответствует имени свойства. Мы устанавливаем значения из модели, чтобы отображались актуальные значения. Но если вы хотите, чтобы форма могла сохранять изменения, вам понадобится метод OnPost:
public void OnPost()
{
var UserId = Settings.UserId;
if (UserId is null)
{
throw new InvalidDataException("UserId can't be null");
}
}
При отправке формы будет выброшено исключение. Почему это не работает? Потому что мы не привязали модель. Есть несколько способов сделать это. Во-первых, мы можем просто принять объект UserSettings в методе OnPost:
public void OnPost(UserSettings settings)
{
var UserId = settings.UserId;
if (UserId is null)
{
throw new InvalidDataException("UserId can't be null");
}
}
Это работает, потому что данные в форме совпадают со свойствами в UserSettings. Но мы не всегда хотим передавать настройки таким образом. Иногда нам нужно привязать их непосредственно к классу PageModel. Для этого мы можем добавить атрибут [BindProperty]:
[BindProperty]
public UserSettings Settings { get; set; } = new UserSettings();
public void OnPost()
{
var UserId = Settings.UserId;
if (UserId is null)
{
throw new InvalidDataException("UserId can't be null");
}
}
В этом случае функция OnPost (или OnPut, или OnDelete) получает значения и привязывает их к инстансу класса. Таким образом, можно упростить работу с большими наборами данных на нашей Razor-странице. Но создание формы должно быть еще проще. Давайте посмотрим, как это сделать.
TagHelpers
При создании формы вам может понадобиться более простая установка значений и привязываемых свойств (а также соответствующих имен). Для этого Microsoft поддерживает так называемые тег-хелперы (TagHelpers). Тег-хелперы позволяют легче привязываться к PageModel. Например, вы можете использовать атрибут asp-for, чтобы указать форме, какие свойства вы хотите привязать:
<form method="POST">
<label asp-for="Settings.UserId">Email</label>
<input asp-for="Settings.UserId" />
<label asp-for="Settings.FullName">
Full Name
</label>
<input asp-for="Settings.FullName" />
<button type="submit">Send...</button>
</form>
Хоть тег-хелперы обычно сами подключаются к новому, я считаю, что вам важно знать, откуда они берутся. Существует специальный файл под названием _ViewImports.cshtml. Он может содержать любые пространства имен, которые вы хотите включить в ваши Razor-страницы (например, YourProject.Pages), и именно в него добавляются тег-хелперы:
@* _ViewImports.cshtml *@
@namespace CodeRazorPages.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Если у вас нет доступа к тег-хелперам, значит, этот файл отсутствует или неверен. Теперь, когда мы разобрались, как работают отдельные страницы, давайте рассмотрим, как лучше составлять страницы из отдельных компонентов.
Компоновка Razor-страниц
Как и в Web Forms, в Razor Pages есть разделение страницы на отдельные части, такие как UserControls и MasterPages. Давайте посмотрим, как это выглядит в Razor Pages.
Использование макетов
Определять всю HTML-страницу в каждой Razor-странице было бы пустой тратой времени. Подобно веб-формам, у вас есть мастер-страницы, которые в Razor Pages называются макетами (Layouts). Макеты хранятся в папке Shared (эта папка является частью пути сразу нескольких типов файлов (например, partials, layouts и т. д.)). Таким образом, они доступны каждой странице, которой они нужны. По соглашению макет называется _Layout, как показано на рисунке 4.
Макет обычно содержит HTML-шаблон, который вы хотите видеть на каждой странице. Вы можете вызвать RenderBody(), чтобы указать макету, где должно отображаться содержимое страницы:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/css/site.css" rel="stylesheet" />
<title>Razor Page</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
Хоть этот пример и достаточно примитивен, вы можете представить, что здесь также будут определены навигация и колонтитулы. Чтобы использовать макет, достаточно просто установить на странице свойство Layout:
@page
@{
Layout = "_layout";
}
<h1>Razor Page</h1>
<p>Today is @DateTime.Now.ToShortDateString()</p>
Необходимость добавлять это на каждую страницу вряд ли обрадует кого-либо, поэтому у нас есть возможность создать специальный файл _ViewStart.cshtml. Этот файл используется для указания общих для каждой Razor-страницы вещей. В данном случае мы можем просто перенести настройки макета в этот файл, и они будут применяться ко всем нашим Razor-страницам. Этот файл должен находиться в папке Pages (он применяется ко всей этой папке и всем ее вложенным папкам), как показано на рисунке 5:
Теперь, когда у нас есть макет, нам может понадобиться обмениваться с ним информацией. Наиболее распространенным сценарием здесь является добавление в заголовок тега title. Для этого можно использовать пакет свойств под названием ViewData. Если вы установите это свойство на Razor-странице, вы сможете получить к нему доступ в макете. Например, на нашей Razor-странице:
@page
@{
ViewData["title"] = "Razor Pages Example";
}
<h1>@ViewData["title"]</h1>
<p>Today is @DateTime.Now.ToShortDateString()</p>
Обратите внимание, что несмотря на то, что мы устанавливаем его здесь, мы можем использовать его и на своей странице. Мы можем просто обратиться к объекту ViewData в нашем макете:
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/css/site.css" rel="stylesheet" />
<title>
CODE Magazine - @ViewData["title"]
</title>
</head>
ViewData также может быть добавлен в какой-нибудь контент, а не только служить в качестве всего контента. В этом примере заголовок каждой страницы начинается с некоторого шаблона, но затем каждая страница может указать свой собственный заголовок.
Кроме того, макеты поддерживают концепцию секций (Sections). Это область страницы, которую Razor-страница сама может вставить в макет. Например:
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<link href="~/css/site.css" rel="stylesheet" />
<title>
CODE Magazine - @ViewData["title"]
</title>
@RenderSection("head", false)
</head>
Вы можете вызвать @RenderSection, чтобы указать секцию с любым именем. Второй (опциональный) параметр определяет, является ли секция обязательной. Есть два распространенных сценария использования этого параметра — позволить отдельной странице внедрять собственные стилистику или скрипты. Но вы можете использовать его для любого раздела страницы. Чтобы задействовать секцию, нам нужно использовать ключевое слово section в файле .cshtml:
@page
@{
ViewData["title"] = "Contact Page";
}
@section head {
<link src="./someextracss" rel="stylesheet" />
}
<h1>Contact Us</h1>
Вы можете увидеть @section в верхней части файла. Имя секции указывается для того, чтобы определить, в какую секцию вы хотите производить вставку. Все, что находится внутри фигурных скобок, вставляется в Layout.
Частичные представления
Иногда нам нужно иметь возможность повторно использовать какой-нибудь часто встречающийся фрагмент разметки. Razor Pages поддерживает это с помощью частичных представлений (Partials). Частичные представления позволяют внедрить общий файл с синтаксисом Razor в существующую страницу. Мы можем взять форму, созданную ранее на странице UserSettings, и перенести ее в частичную страницу. Для этого мы создадим cshtml-файл с префиксом в виде нижнего подчеркивания.
Это не обязательно, но так легче понять, какие страницы являются полноценными, а какие — частичными. Например, если вы создадите файл _SettingsForm.cshtml следующим образом:
<form method="POST">
<label asp-for="Settings.UserId">Email</label>
<input asp-for="Settings.UserId" />
<label asp-for="Settings.FullName">
Full Name
</label>
<input asp-for="Settings.FullName" />
<button type="submit">Send...</button>
</form>
Обратите внимание, что в этом новом файле нет объявления @page; в этом нет необходимости. Чтобы использовать эту частичную страницу, достаточно использовать элемент partial:
@page
@{
ViewData["title"] = "Contact Page";
}
<h1>Contact Us</h1>
<div>
<partial name="_ContactForm" />
</div>
Обратите внимание, что имя представления — это имя без расширения. Это не путь к частичному представлению, а имя, которое мы можем найти. При поиске частичного представления система ищет его в нескольких местах (по порядку):
В том же каталоге, что и Razor-страница, содержащая частичное представление
Любой родительский для каталога страницы каталог
Каталог /Shared
Каталог /Pages/Shared
Каталог /Views/Shared
Если частичное представление не будет найдено здесь, возвращается ошибка. Если это происходит, то либо имя неверно, либо оно находится в таком месте, где Razor Pages не может его найти.
Вы можете вполне законно полагать, что частичные страницы очень похожи на обычные Razor-страницы. Это означает, что они имеют данные, которые можно им передать, и могут использовать синтаксис Razor для их изменения. В этом примере, чтобы заполнить форму, нам нужно передать ей UserSettings. Для этого используется атрибут model:
@page
@model UserSettingsModel
<h1>Settings</h1>
<partial name="_SettingsForm" model="Model.Settings" />
Как только мы это сделаем, нам нужно будет установить model на частичной странице:
@model UserSettings
<form method="POST">
<label asp-for="UserId">Email</label>
<input asp-for="UserId" />
<label asp-for="FullName">Full Name</label>
<input asp-for="FullName" />
<button type="submit">Send...</button>
</form>
Обратите внимание, что это необходимо только потому, что здесь используется привязка к модели. Если бы это был просто фрагмент HTML, то нам не нужно было бы делать даже это. Используя эти нехитрые механизмы, вы получаете возможность создавать полноценные серверные веб-проекты. На этом мы пожалуй и закончим.
К чему мы в итоге пришли?
Razor Pages — это модель программирования веб-разработки, ориентированная на страницы, похожая в работе на платформу Web Forms. Хоть вы можете применить многое из своего опыта работы с Web Forms непосредственно в Razor Pages, есть и существенные различия. Если вы привыкли работать как дизайнер и задавать свойства в пользовательском интерфейсе, вам все равно придется разобраться с тем, как на самом деле работает HTML. Поскольку вы здесь больше не оперируете состоянием сеанса, вам придется изменить свое мышление. Но если вы готовы потратить время на перенос своих навыков работы с C# на .NET Core, Razor Pages — отличная отправная точка.
Исходный код
Исходный код можно загрузить с https://github.com/wilder-minds/CodeRazorPages и со страницы www.CODEMag.com, посвященной этой статье.
Материал подготовлен в рамках практического онлайн-курса «C# ASP.NET Core разработчик».