[Перевод] Гладкое бритье: Razor Pages для разработчиков веб-форм

Если вы, будучи разработчиком 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 лежат две довольно простые концепции:

Имеется в виду, что при создании нового проекта с помощью 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.

Figure 1: Razor Pages middleware

Рисунок 1: Middleware Razor Pages

Как она узнает, существует ли страница Razor Page для данного запроса? Она использует соглашение для поиска файлов. Хотя конкретная реализация может немного отличаться, вы можете представить папку Pages как корень веб-сервера. Это означает, что для ответа на запросы Razor Pages будет следовать структуре папка/файл. Например, если URL запроса — /contact, middleware будет искать в папке Pages файл с именем contact.cshtml. Если он будет найден, то она сгенерирует эту страницу и вернет ее клиенту, как показано на рисунке 2.

Figure 2: Looking for a Razor Page

Рисунок 2: Поиск Razor Page

Исключением является файл Index.cshtml. Этот файл используется, когда URL указывает непосредственно на папку, а не на имя отдельного файла. Это имя страницы по умолчанию и запасной вариант. Это означает, что если URL выглядит как https://localhost/, он будет искать index.cshtml.

Папки работают аналогичным образом. Любые папки внутри папки Pages структурно соответствуют фрагментам URL. Например, если у вас есть URL типа /Sales/ProductList, соответствующий ему файл будет выглядеть, как показано на рисунке 3.

Figure 3: Folders in Razor Pages

Рисунок 3: Папки в Razor Pages

Итак, теперь, когда вы видите, как отображаются Razor-страницы, давайте разберемся, из чего они состоят.

Анатомия Razor Page

Хоть в большинстве случаев вы будете использовать для создания Razor-страниц существующие заготовки, давайте рассмотрим, что делает Razor-страницу Razor-страницей. Razor Page — это просто файл, содержащий объявление @page:

@page 



    
    
    
    Document


    

Hello from Razor

Таким образом мы сигнализируем middleware, что это обслуживаемый файл. Папка Pages может содержать другие файлы, например, файлы макета (layout) или частичных (partial) представлений, которые вы не хотите отображать как отдельные страницы. Это помогает middleware определить, действительно ли это Razor Page или нет. Знак »@» не является случайностью. В синтаксисе Razor символ »@» используется для обозначения начала операции серверного кода. Например, вы можете создать произвольный блок кода следующим образом:

@page
@{
    var title = "This is made with Razor Pages";
}

Фигурные скобки (т.е. {}) нужны только для того, чтобы сделать код многострочным. Этот код — обычный C# (или VB.NET, если вы используете файлы .vbhtml). Но опять же, начинать его нужно с символа »@»:

@page
@{
    var title = "CODE Magazine - Razor Pages";
}



    
    
    
    
    @title


    

@title

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 также позволяет вызывать методы или свойства, поскольку все, что следует за знаком @, интерпретируется в рамках языка файла:

Today is @DateTime.Now.ToShortDateString()

Вы даже можете использовать операторы управления потоком выполнения:




    
    
    
    
    @title


    
@if (string.IsNullOrEmpty(@title) == false) {

@title

}

Чтобы определить, показывать ли 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
...

@Model.Title

Today is @DateTime.Now.ToShortDateString()

...

Объявление @model дает нам доступ к свойству Model, которая является инстансом класса IndexModel. Как видите, с помощью синтаксиса Razor можно внедрить на страницу Title, который вы определили в классе PageModel.

С классом PageModel можно реализовывать и более сложные взаимодействия. Вы могли заметить, что класс PageModel содержит метод OnGet. Этот метод выполняется перед отрисовкой страницы, если она была получена с помощью метода GET. Таким образом, в нашем примере можно сделать что-то вроде этого:

public class IndexModel : PageModel
{
    public string Title { get; set; } = "Welcome";
    public List InvoiceTotals { get; set; } = new List();
    public void OnGet()
    {
        for (var x = 0; x < 10; ++x)
        {
            InvoiceTotals.Add(Random.Shared.NextDouble() * 100);
        }
    }
}

Таким образом мы можем создавать контент (в данном случае случайные итоговые суммы), а затем использовать его:

Model.Title

Today is @DateTime.Now.ToShortDateString()

Invoices

@foreach (var invoice in Model.InvoiceTotals) {
$ @invoice.ToString("0.00")
}

Хоть это и достаточно сложный пример, но вы можете представить себе сценарий, где мы считываем что-либо из базы данных, чтобы отобразить это точно таким же образом. Мы бы хотели делать это в методе 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:

Settings

Также обратите внимание, что указанное имя соответствует имени свойства. Мы устанавливаем значения из модели, чтобы отображались актуальные значения. Но если вы хотите, чтобы форма могла сохранять изменения, вам понадобится метод 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, чтобы указать форме, какие свойства вы хотите привязать:

Хоть тег-хелперы обычно сами подключаются к новому, я считаю, что вам важно знать, откуда они берутся. Существует специальный файл под названием _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.

Figure 4: Location of the Layouts

Рисунок 4: Расположение макетов

Макет обычно содержит HTML-шаблон, который вы хотите видеть на каждой странице. Вы можете вызвать RenderBody (), чтобы указать макету, где должно отображаться содержимое страницы:




    
    
    
    
    Razor Page


   
@RenderBody()

Хоть этот пример и достаточно примитивен, вы можете представить, что здесь также будут определены навигация и колонтитулы. Чтобы использовать макет, достаточно просто установить на странице свойство Layout:

@page
@{
    Layout = "_layout";
}

Razor Page

Today is @DateTime.Now.ToShortDateString()

Необходимость добавлять это на каждую страницу вряд ли обрадует кого-либо, поэтому у нас есть возможность создать специальный файл _ViewStart.cshtml. Этот файл используется для указания общих для каждой Razor-страницы вещей. В данном случае мы можем просто перенести настройки макета в этот файл, и они будут применяться ко всем нашим Razor-страницам. Этот файл должен находиться в папке Pages (он применяется ко всей этой папке и всем ее вложенным папкам), как показано на рисунке 5:

Figure 5: Location of the _ViewStart.cshtml file

Рисунок 5: Расположение файла _ViewStart.cshtml

Теперь, когда у нас есть макет, нам может понадобиться обмениваться с ним информацией. Наиболее распространенным сценарием здесь является добавление в заголовок тега title. Для этого можно использовать пакет свойств под названием ViewData. Если вы установите это свойство на Razor-странице, вы сможете получить к нему доступ в макете. Например, на нашей Razor-странице:

@page
@{
    ViewData["title"] = "Razor Pages Example";
}

@ViewData["title"]

Today is @DateTime.Now.ToShortDateString()

Обратите внимание, что несмотря на то, что мы устанавливаем его здесь, мы можем использовать его и на своей странице. Мы можем просто обратиться к объекту ViewData в нашем макете: CODE Magazine - @ViewData["title"]

ViewData также может быть добавлен в какой-нибудь контент, а не только служить в качестве всего контента. В этом примере заголовок каждой страницы начинается с некоторого шаблона, но затем каждая страница может указать свой собственный заголовок.

Кроме того, макеты поддерживают концепцию секций (Sections). Это область страницы, которую Razor-страница сама может вставить в макет. Например:


    
    
    
    
    
        CODE Magazine - @ViewData["title"]
    
    @RenderSection("head", false)

Вы можете вызвать @RenderSection, чтобы указать секцию с любым именем. Второй (опциональный) параметр определяет, является ли секция обязательной. Есть два распространенных сценария использования этого параметра — позволить отдельной странице внедрять собственные стилистику или скрипты. Но вы можете использовать его для любого раздела страницы. Чтобы задействовать секцию, нам нужно использовать ключевое слово section в файле .cshtml:

@page
@{
    ViewData["title"] = "Contact Page";
}
@section head {
    
}

Contact Us

Вы можете увидеть @section в верхней части файла. Имя секции указывается для того, чтобы определить, в какую секцию вы хотите производить вставку. Все, что находится внутри фигурных скобок, вставляется в Layout.

Частичные представления

Иногда нам нужно иметь возможность повторно использовать какой-нибудь часто встречающийся фрагмент разметки. Razor Pages поддерживает это с помощью частичных представлений (Partials). Частичные представления позволяют внедрить общий файл с синтаксисом Razor в существующую страницу. Мы можем взять форму, созданную ранее на странице UserSettings, и перенести ее в частичную страницу. Для этого мы создадим cshtml-файл с префиксом в виде нижнего подчеркивания.

Это не обязательно, но так легче понять, какие страницы являются полноценными, а какие — частичными. Например, если вы создадите файл _SettingsForm.cshtml следующим образом:

Обратите внимание, что в этом новом файле нет объявления @page; в этом нет необходимости. Чтобы использовать эту частичную страницу, достаточно использовать элемент partial:

@page
@{
    ViewData["title"] = "Contact Page";
}

Contact Us

Обратите внимание, что имя представления — это имя без расширения. Это не путь к частичному представлению, а имя, которое мы можем найти. При поиске частичного представления система ищет его в нескольких местах (по порядку):

  • В том же каталоге, что и Razor-страница, содержащая частичное представление

  • Любой родительский для каталога страницы каталог

  • Каталог /Shared

  • Каталог /Pages/Shared

  • Каталог /Views/Shared

Если частичное представление не будет найдено здесь, возвращается ошибка. Если это происходит, то либо имя неверно, либо оно находится в таком месте, где Razor Pages не может его найти.

Вы можете вполне законно полагать, что частичные страницы очень похожи на обычные Razor-страницы. Это означает, что они имеют данные, которые можно им передать, и могут использовать синтаксис Razor для их изменения. В этом примере, чтобы заполнить форму, нам нужно передать ей UserSettings. Для этого используется атрибут model:

@page
@model UserSettingsModel

Settings

Как только мы это сделаем, нам нужно будет установить model на частичной странице:

@model UserSettings

Обратите внимание, что это необходимо только потому, что здесь используется привязка к модели. Если бы это был просто фрагмент 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 разработчик».

© Habrahabr.ru