Введение в Razor Pages в ASP.NET Core

Авторы: Рик Андерсон (Rick Anderson) и Райан Новак (Ryan Nowak)

Razor Pages делает создание кодов сценариев для страниц проще и эффективнее по сравнению с использованием контроллеров и представлений.

Если вам нужно руководство, использующее подход "модель-представление-контроллер", см. статью Начало работы с MVC в ASP.NET Core.

Этот документ содержит вводные сведения о Razor Pages. Это не пошаговое руководство. Если некоторые разделы покажутся вам слишком сложными, см. Начало работы с Razor Pages. Общие сведения об ASP.NET Core см. в разделе Введение в ASP.NET Core.

Предварительные требования

Создание проекта Razor Pages

Подробные инструкции по созданию проекта Razor Pages см. в статье Начало работы с Razor Pages.

Razor Pages

Razor Pages активируется в файле Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Рассмотрим простую страницу:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

Приведенный выше код выглядит как Razor файл представления, используемый в приложениях ASP.NET Core с контроллерами и представлениями. Он отличается от него только директивой @page. Директива @page превращает файл в действие MVC, а значит обрабатывает запросы напрямую, минуя контроллер. @page должна быть первой директивой Razor на странице. @page влияет на поведение всех остальных конструкций Razor. Имена файлов Razor Pages имеют суффикс .cshtml.

Похожая страница с использованием класса PageModel показана в следующих двух файлах. Файл Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Модель страницы Pages/Index2.cshtml.cs:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

Как правило, файл класса PageModel называется так же, как файл Razor Pages, но с расширением CS. Например, представленная выше страница Razor Pages называется Pages/Index2.cshtml. Файл, содержащий класс PageModel, называется Pages/Index2.cshtml.cs.

Сопоставления URL-адресов со страницами определяются расположением конкретной страницы в файловой системе. В приведенной ниже таблице показаны пути Razor Pages и соответствующие URL-адреса.

Имя файла и путь Соответствующий URL
/Pages/index.cshtml / или /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store или /Store/Index

Примечания.

  • Среда выполнения по умолчанию ищет файлы Razor Pages в папке Pages.
  • Если в URL-адресе не указана конкретная страница, по умолчанию открывается страница Index.

Создание простой формы

Razor Pages предназначена для упрощения реализации типовых шаблонов, которые используются в браузерах, при создании приложения. Привязки модели, вспомогательные функции тегов и вспомогательные методы HTML отлично работают со свойствами, определенными в классе Razor Pages. Рассмотрим страницу с простой формой связи для модели Contact.

В представленных в этой статье примерах DbContext инициализируется в файле Startup.cs.

Для работы с базой данных в памяти требуется пакет NuGet Microsoft.EntityFrameworkCore.InMemory.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<CustomerDbContext>(options =>
                      options.UseInMemoryDatabase("name"));
    services.AddRazorPages();
}

Модель данных:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

Контекст базы данных:

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Models;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
    }
}

Файл представления Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Модель страницы Pages/Create.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using RazorPagesContacts.Models;
using System.Threading.Tasks;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateModel : PageModel
    {
        private readonly CustomerDbContext _context;

        public CreateModel(CustomerDbContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Customers.Add(Customer);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}

Как правило, класс PageModel называется <PageName>Model и находится в том же пространстве имен, что и страница.

Класс PageModel позволяет разделять логику страницы и ее представление. Он определяет обработчики страницы для запросов, отправляемых на страницу, а также данные для ее визуализации. Это разделение позволяет:

Страница содержит метод обработчика OnPostAsync, который выполняется по запросам POST (когда пользователь публикует форму). Можно добавить методы обработчика для любой HTTP-команды. Наиболее распространенные обработчики

  • OnGet — инициализация необходимого для страницы состояния. В приведенном выше коде метод OnGet отображает страницу Razor CreateModel.cshtml.
  • OnPost — обработка отправленных через форму данных.

Суффикс Async не является обязательным, но часто используется для асинхронных функций. Этот код типичен для Razor Pages.

Если вы знакомы с приложениями ASP.NET, использующими контроллеры и представления:

  • Код OnPostAsync в предыдущем примере похож на стандартный код контроллера.
  • Большинство примитивов MVC, включая привязку модели, проверку и результаты действий, одинаково работают с контроллерами и Razor Pages.

Предыдущий метод OnPostAsync:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Customers.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Простая схема OnPostAsync:

Проверка на наличие ошибок проверки.

  • Если ошибок нет, сохранение данных и перенаправление.
  • Если есть ошибки, отображение страницы с сообщениями проверки. Во многих случаях ошибки проверки выявляются на клиентском компьютере и на сервер не отправляются.

Файл представления Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Преобразованный HTML из Pages/Create.cshtml:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

В приведенном выше коде, разместив форму:

  • С допустимыми данными:

    • Метод обработчика OnPostAsync вызывает вспомогательный метод RedirectToPage. RedirectToPage возвращает экземпляр RedirectToPageResult. RedirectToPage:

      • Является результатом действия.
      • Аналогичен RedirectToAction или RedirectToRoute (используется в контроллерах и представлениях).
      • Настраивается для страниц. В приведенном выше примере он выполняет перенаправление на корневую страницу индекса (/Index). Более подробно RedirectToPage рассматривается в разделе Создание URL для страниц.
  • С ошибками проверки, передаваемыми на сервер:

    • Метод обработчика OnPostAsync вызывает вспомогательный метод Page. Page возвращает экземпляр PageResult. Возвращение Page аналогично тому, как действия в контроллерах возвращают View. PageResult — тип возвращаемого значения по умолчанию для метода обработчика. Метод обработчика, вернувший void, визуализирует страницу.
    • В предыдущем примере публикация формы без значения приводит к тому, что ModelState.IsValid возвращает значение false. В этом примере на клиенте не отображаются ошибки проверки. Обработка ошибок проверки рассматривается далее в этом документе.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • С ошибками проверки, обнаруженными при проверке на стороне клиента:

    • Данные не отправляются на сервер.
    • Проверка на стороне клиента описана далее в этом документе.

Для указания согласия на привязку модели в свойстве Customer используется атрибут [BindProperty]:

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

[BindProperty]не следует использовать в моделях, содержащих свойства, которые не должны изменяться клиентом. Дополнительные сведения см. в этом разделе.

По умолчанию Razor привязывает свойства ко всем командам, кроме GET. Привязка к свойствам устраняет необходимость в написании кода для преобразования данных HTTP в тип модели. Привязка уменьшает код за счет того, что для визуализации полей формы (<input asp-for="Customer.Name">) и получения входных данных используется одно и то же свойство.

Предупреждение

В целях безопасности вам следует задать привязку данных запроса GET к свойствам страничной модели. Проверьте введенные данные пользователя, прежде чем сопоставлять их со свойствами. Привязка GET полезна при обращении к сценариям, использующим строку запроса или значения маршрутов.

Чтобы привязать свойство к запросам GET, задайте для свойства SupportsGet атрибута [BindProperty] значение true:

[BindProperty(SupportsGet = true)]

Дополнительные сведения: ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

Просмотр файла представления Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Домашняя страница

Index.cshtml — это домашняя страница:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customer)
            {
                <tr>
                    <td> @contact.Id  </td>
                    <td>@contact.Name</td>
                    <td>
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <button type="submit" asp-page-handler="delete"
                                asp-route-id="@contact.Id">delete
                        </button>
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

Связанный класс PageModel (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly CustomerDbContext _context;

    public IndexModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer> Customer { get; set; }

    public async Task OnGetAsync()
    {
        Customer = await _context.Customers.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customers.FindAsync(id);

        if (contact != null)
        {
            _context.Customers.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

Файл index.cshtml содержит следующую разметку:

<td>

Вспомогательный тег привязки <a /a> использовал атрибут asp-route-{value} для создания ссылки на страницу редактирования. Эта ссылка содержит данные о маршруте с идентификатором контактного лица. Например, https://localhost:5001/Edit/1. Вспомогательные функции тегов позволяют серверному коду участвовать в создании и отображении HTML-элементов в файлах Razor.

Файл Index.cshtml содержит разметку для создания кнопки удаления у каждого контакта клиента:

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
<button type="submit" asp-page-handler="delete"

Отображаемый HTML:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

Во время обработки кнопки удаления в HTML ее formaction включает параметры для следующего:

  • Идентификатор контакта клиента, указанный атрибутом asp-route-id.
  • Параметр handler, указанный атрибутом asp-page-handler.

При выборе кнопки на сервер отправляется запрос формы POST. По соглашению имя метода обработчика выбирается на основе значения параметра handler в соответствии со схемой OnPost[handler]Async.

Так как handler — delete в этом примере, метод обработчика OnPostDeleteAsync используется для обработки запроса POST. Если asp-page-handler имеет другое значение, например remove, выбирается метод обработчика с именем OnPostRemoveAsync.

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customers.FindAsync(id);

    if (contact != null)
    {
        _context.Customers.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

Метод OnPostDeleteAsync:

  • Получает id из строки запроса.
  • Отправляет в базу данных запрос контакта клиента с FindAsync.
  • Если контакт клиента найден, он удаляется, и база данных обновляется.
  • Вызывает RedirectToPage для перенаправления на корневую страницу индекса (/Index).

Файл Edit.cshtml

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name"></span>
        </div>
    </div>

    <div>
        <button type="submit">Save</button>
    </div>
</form>

Первая строка содержит директиву @page "{id:int}". Ограничение маршрутизации "{id:int}" указывает, что страница должна принимать обращенные к ней запросы, которые содержат данные маршрутизации int. Если запрос к странице не содержит данные о маршруте, которые можно конвертировать в int, среда выполнения возвращает ошибку HTTP 404 (не найдено). Чтобы сделать идентификатор необязательным, добавьте ? к ограничению маршрута:

@page "{id:int?}"

Файл Edit.cshtml.cs:

public class EditModel : PageModel
{
    private readonly CustomerDbContext _context;

    public EditModel(CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Customer = await _context.Customers.FindAsync(id);

        if (Customer == null)
        {
            return RedirectToPage("./Index");
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Customer).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new Exception($"Customer {Customer.Id} not found!");
        }

        return RedirectToPage("./Index");
    }

}

Проверка

Правила проверки:

  • Декларативно задаются в классе Model.
  • Применяются везде в приложении.

Пространство имен System.ComponentModel.DataAnnotations предоставляет набор встроенных атрибутов проверки, которые декларативно применяются к классу или свойству. Кроме того, DataAnnotations содержит атрибуты форматирования (такие как [DataType]), которые обеспечивают форматирование и не предназначены для проверки.

Рассмотрим модель Customer:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

Используя следующий файл представления Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

Предыдущий код:

  • Включает jQuery и скрипты проверки jQuery.

  • Использует вспомогательные функции тегов <div /> и <span /> для следующих задач:

    • Проверка на стороне клиента.
    • Отображение ошибок при проверке.
  • Генерирует следующий HTML:

    <p>Enter a customer name:</p>
    
    <form method="post">
        Name:
        <input type="text" data-val="true"
               data-val-length="The field Name must be a string with a maximum length of 10."
               data-val-length-max="10" data-val-required="The Name field is required."
               id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
        <input type="submit" />
        <input name="__RequestVerificationToken" type="hidden"
               value="<Antiforgery token here>" />
    </form>
    
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
    

При публикации формы создания без значения имени в форме отображается сообщение об ошибке: "Поле имени является обязательным". в форме. Если на клиенте включен JavaScript, браузер отображает ошибку без отправки на сервер.

Атрибут [StringLength(10)] создает data-val-length-max="10" в отображаемом HTML-коде. data-val-length-max не дает браузерам ввести больше заданной максимальной длины. Если для изменения и воспроизведения записи используется средство, например Fiddler, выполните следующие действия:

  • С именем, превышающим 10.
  • Возвращается сообщение об ошибке: "Имя поля должно быть строкой с максимальной длиной 10". .

Рассмотрим следующую модель Movie:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}

Атрибуты проверки определяют поведение для свойств модели, к которым они применяются:

  • Атрибуты Required и MinimumLength указывают, что свойство должно иметь значение. Тем не менее, чтобы удовлетворить требованиям проверки, пользователю достаточно ввести пробел.

  • Атрибут RegularExpression ограничивает набор допустимых для ввода символов. В приведенном выше коде в Genre:

    • должны использоваться только буквы;
    • первая буква должна быть прописной; пробелы, цифры и специальные символы не допускаются.
  • В RegularExpression Rating:

    • первый символ должен быть прописной буквой;
    • допускаются специальные символы и цифры, а также последующие пробелы. Значение "PG-13" допустимо для рейтинга, но недопустимо для жанра.
  • Атрибут Range ограничивает диапазон значений.

  • Атрибут StringLength задает максимальную и при необходимости минимальную длину строкового свойства.

  • Типы значений (например, decimal, int, float, DateTime) по своей природе являются обязательными и не требуют атрибута [Required].

На странице создания для модели Movie отображаются ошибки с недопустимыми значениями:

Форма просмотра фильма с несколькими ошибками проверки jQuery на стороне клиента

Дополнительные сведения можно найти в разделе

Обработка запросов HEAD с помощью вызова резервного обработчика OnGet

Запросы HEAD позволяют получать заголовки для определенного ресурса. В отличие от запросов GET запросы HEADне возвращают текст ответа.

Обработчик OnHead обычно создается и вызывается для выполнения запросов HEAD:

public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

Если обработчик OnHead не определен, Razor Pages выполнит вызов обработчика OnGet.

XSRF/CSRF и Razor Pages

В Razor Pages реализована проверка для защиты от подделки. FormTagHelper вставляет маркеры защиты от подделки в элементы HTML-форм.

Использование макетов, частичных реплик, шаблонов и вспомогательных функций тегов с Razor Pages

Pages работает со всеми функциями подсистемы просмотра Razor. Макеты, частичные реплики, шаблоны, вспомогательные функции тегов, а также файлы _ViewStart.cshtml и _ViewImports.cshtml работают точно так же, как и в стандартных представлениях Razor.

Давайте упростим нашу страницу с помощью некоторых из этих функций.

Добавим макет страницы в файл Pages/Shared/_Layout.cshtml.

<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

Этот макет:

  • управляет макетом каждой страницы (кроме страниц с отказом от макета);
  • импортирует HTML-структуры, такие как JavaScript и таблицы стилей.
  • Содержимое страницы Razor отображается в том месте, где вызывается @RenderBody().

Дополнительные сведения см. здесь.

Свойство Layout определяется в файле Pages/_ViewStart.cshtml:

@{
    Layout = "_Layout";
}

Макет хранится в папке Pages/Shared. Pages ищет другие представления (макеты, шаблоны, частичные реплики) в иерархическом порядке, начиная с той папки, где находится текущая страница. Макет в папке Pages/Shared можно использовать на любой странице Razor, которая находится в папке Pages.

Файл макета следует поместить в папку Pages/Shared.

Корпорация Майкрософт рекомендует не размещать файл макета в папке Views/Shared. Views/Shared — это шаблон представлений MVC. Razor Pages опирается на иерархию папок, а не на условные обозначения путей.

Поиск представлений в Razor Pages охватывает папку Pages. Макеты, шаблоны и частичные реплики работают с контроллерами MVC и стандартными представлениями Razor.

Добавим файл Pages/_ViewImports.cshtml:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace описывается далее в этом руководстве. Директива @addTagHelper добавляет встроенные вспомогательные теги на все страницы в папке Pages.

Директива @namespace, заданная на странице:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

Директива @namespace задает пространство имен для страницы. Включать пространство имен в директиву @model не требуется.

Если директива @namespace содержится в файле _ViewImports.cshtml, указанное пространство имен определяет префикс для созданного в Pages пространства имен, куда импортируется директива @namespace. Остальная часть созданного пространства имен (суффикс) представляет собой разделенный точками относительный путь между папкой с файлом _ViewImports.cshtml и папкой, содержащей страницу.

Например, класс PageModel в файле Pages/Customers/Edit.cshtml.cs задает пространство имен явно.

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

Файл Pages/_ViewImports.cshtml задает следующее пространство имен:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Сформированное пространство имен для файла Pages/Customers/Edit.cshtml Razor Pages совпадает с пространством имен класса PageModel.

@namespace также работает со стандартными представлениями Razor.

Рассмотрим файл представления Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

Обновленный файл представления Pages/Create.cshtml с _ViewImports.cshtml и предыдущим файлом макета:

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

В приведенном выше коде _ViewImports. cshtml импортировал пространство имен и вспомогательные функции тегов. Файл макета импортировал файлы JavaScript.

Начальный проект Razor Pages содержит файл Pages/_ValidationScriptsPartial.cshtml, который подключает проверку на стороне клиента.

Дополнительные сведения о частичных представлениях см. в Частичные представления в ASP.NET Core.

Формирование URL-адресов для страниц

На представленной выше странице Create используется RedirectToPage:

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Это приложение имеет следующую структуру файлов и папок:

  • /Pages

    • Index.cshtml

    • PrivacyCSHTML

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

После успешного выполнения страницы Pages/Customers/Create.cshtml и Pages/Customers/Edit.cshtml перенаправляются на страницу Pages/Customers/Index.cshtml. Строка ./Index — это относительное имя страницы, используемое для доступа к предыдущей странице. Эта строка позволяет создавать URL-адреса для страницы Pages/Customers/Index.cshtml. Пример:

  • Url.Page("./Index", ...)
  • <a asp-page="./Index">Customers Index Page</a>
  • RedirectToPage("./Index")

С помощью абсолютного имени страницы /Index создаются URL-адреса страницы Pages/Index.cshtml. Пример:

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">Home Index Page</a>
  • RedirectToPage("/Index")

Имя страницы — это путь к странице из корневой папки /Pages, включая начальный символ / (например, /Index). Предыдущие образцы создания URL-адреса обеспечивают расширенные параметры и функциональные возможности по сравнению с жестким заданием URL-адреса. Формирование URL-адресов включает маршрутизацию и позволяет генерировать и включать в код параметры в зависимости от того, как определяется маршрут в пути назначения.

Формирование URL-адресов для страниц поддерживает относительные имена. В приведенной ниже таблице показано, какая страница индекса выбирается с помощью разных параметров RedirectToPage в Pages/Customers/Create.cshtml.

RedirectToPage(x) Страница
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index"), RedirectToPage("./Index") и RedirectToPage("../Index") — это относительные имена. Для получения имени целевой страницы параметр RedirectToPageкомбинируется с путем текущей страницы.

Привязка относительных имен полезна при создании сайтов со сложной структурой. Если относительные имена используются для связи между страницами в папке:

  • Переименование папки не нарушает относительные ссылки.
  • Ссылки не нарушаются, так как не содержат имя папки.

Чтобы выполнить перенаправление на страницу в другой области, укажите эту область:

RedirectToPage("/Index", new { area = "Services" });

Дополнительные сведения см. в разделах Области в ASP.NET Core и Соглашения для маршрутов и приложений Razor Pages в ASP.NET Core.

Атрибут ViewData

Данные могут передаваться на страницу с помощью атрибута ViewDataAttribute. Значения свойств с атрибутом [ViewData] хранятся в ViewDataDictionary и загружаются из него.

В следующем примере AboutModel применяет атрибут [ViewData] к свойству Title:

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

На странице About доступ к свойству Title осуществляется как доступ к свойству модели.

<h1>@Model.Title</h1>

В макете заголовок считывается из словаря ViewData.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core предоставляет TempData. Это свойство хранит данные до тех пор, пока они не будут прочитаны. Для проверки данных без удаления можно использовать методы Keep и Peek. TempData удобно использовать для перенаправления, когда данные требуются больше, чем для одного запроса.

В следующем коде значение Message задается с помощью TempData:

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

Представленная ниже разметка в файле Pages/Customers/Index.cshtml отображает значение Message с помощью TempData.

<h3>Msg: @Model.Message</h3>

Модель страницы Pages/Customers/Index.cshtml.cs применяет атрибут [TempData] к свойству Message.

[TempData]
public string Message { get; set; }

Дополнительные сведения см. в разделе TempData.

Несколько обработчиков на страницу

Следующая страница формирует разметку для двух обработчиков с помощью вспомогательной функции тегов asp-page-handler:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

Форма в предыдущем примере включает две кнопки отправки, каждая из которых отправляет данные на отдельный URL-адрес с помощью FormActionTagHelper. Атрибут asp-page-handler является дополнением к asp-page. Атрибут asp-page-handler формирует URL-адреса, ,которые используются для отправки данных в каждый из методов обработчиков, определенных страницей. asp-page не задается, так как пример сопоставлен с текущей страницей.

Модель страницы

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

В представленном выше коде используются именованные методы обработчика. Именованные методы обработчика создаются путем размещения определенного текста в имени после On<HTTP Verb> и перед Async (если есть). В приведенном выше примере использовались методы страницы OnPost JoinList Async и OnPost JoinListUC Async. Если убрать OnPost и Async, имена обработчиков будут выглядеть как JoinList и JoinListUC.

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

При использовании представленного выше кода URL-путь для отправки данных в OnPostJoinListAsync будет выглядеть как https://localhost:5001/Customers/CreateFATH?handler=JoinList. URL-путь для отправки данных в OnPostJoinListUCAsync будет иметь вид https://localhost:5001/Customers/CreateFATH?handler=JoinListUC.

Пользовательские маршруты

С помощью директивы @page можно сделать следующее.

  • Указать пользовательский маршрут к странице. Например, можно задать маршрут к странице "Сведения" /Some/Other/Path: @page "/Some/Other/Path".
  • Добавить сегменты к маршруту страницы по умолчанию. Например, к такому маршруту можно добавить сегмент item: @page "item".
  • Добавить параметры к маршруту страницы по умолчанию. Например, для страницы с @page "{id}" может потребоваться параметр идентификатора id.

Поддерживается путь относительно корня, заданный знаком тильды (~) в начале пути. Например, @page "~/Some/Other/Path" равносильно @page "/Some/Other/Path".

Если вы не хотите, чтобы в URL-адресе отображалась строка запроса ?handler=JoinList, измените маршрут так, чтобы в качестве пути в URL-адресе указывалось имя обработчика. Для настройки маршрута добавьте после директивы @page шаблон маршрута, заключенный в двойные кавычки.

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

При использовании представленного выше кода URL-путь для отправки данных в OnPostJoinListAsync будет выглядеть как https://localhost:5001/Customers/CreateFATH/JoinList. URL-путь для отправки данных в OnPostJoinListUCAsync будет иметь вид https://localhost:5001/Customers/CreateFATH/JoinListUC.

Символ ? после handler означает, что параметр маршрута является необязательным.

Расширенная конфигурация и параметры

Конфигурация и параметры в следующих разделах не требуются большинству приложений.

Для настройки расширенных параметров используйте перегрузку AddRazorPages, которая настраивает RazorPagesOptions:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
    {
        options.RootDirectory = "/MyPages";
        options.Conventions.AuthorizeFolder("/MyPages/Admin");
    });
}

Используйте RazorPagesOptions, чтобы задавать корневой каталог для страниц и добавлять для них соглашения моделей приложений. Дополнительные сведения о соглашениях см. в разделе Соглашения об авторизации Razor Pages.

Сведения о предварительной компиляции представлений см. на странице Компиляция представлений Razor.

Указание местонахождения Razor Pages в корне каталога

По умолчанию Razor Pages находится в корне каталога /Pages. Добавьте WithRazorPagesAtContentRoot, чтобы указать, что Razor Pages находится в корневой папке содержимого (ContentRootPath) приложения:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesAtContentRoot();
}

Указание местонахождения Razor Pages в пользовательском корневом каталоге

Добавьте WithRazorPagesRoot, чтобы указать, что Razor Pages находится в пользовательском корневом каталоге в приложении (укажите относительный путь):

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesRoot("/path/to/razor/pages");
}

Дополнительные ресурсы

Предупреждение

Если вы используете Visual Studio 2017, см. обсуждение dotnet/sdk issue #3124 для получения сведений о версиях пакета SDK для .NET Core, которые не работают с Visual Studio.

Создание проекта Razor Pages

Подробные инструкции по созданию проекта Razor Pages см. в статье Начало работы с Razor Pages.

Razor Pages

Razor Pages активируется в файле Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Includes support for Razor Pages and controllers.
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvc();
    }
}

Рассмотрим простую страницу:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

Приведенный выше код выглядит как файл представления Razor, используемый в приложениях ASP.NET Core с контроллерами и представлениями. и отличается от него только директивой @page. Директива @page превращает файл в действие MVC, а значит обрабатывает запросы напрямую, минуя контроллер. @page должна быть первой директивой Razor на странице. @page влияет на поведение всех остальных конструкций Razor.

Похожая страница с использованием класса PageModel показана в следующих двух файлах. Файл Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model IndexModel2

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Модель страницы Pages/Index2.cshtml.cs:

using Microsoft.AspNetCore.Mvc.RazorPages;
using System;

namespace RazorPagesIntro.Pages
{
    public class IndexModel2 : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

Как правило, файл класса PageModel называется так же, как файл Razor Pages, но с расширением CS. Например, представленная выше страница Razor Pages называется Pages/Index2.cshtml. Файл, содержащий класс PageModel, называется Pages/Index2.cshtml.cs.

Сопоставления URL-адресов со страницами определяются расположением конкретной страницы в файловой системе. В приведенной ниже таблице показаны пути Razor Pages и соответствующие URL-адреса.

Имя файла и путь Соответствующий URL
/Pages/index.cshtml / или /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store или /Store/Index

Примечания.

  • Среда выполнения по умолчанию ищет файлы Razor Pages в папке Pages.
  • Если в URL-адресе не указана конкретная страница, по умолчанию открывается страница Index.

Создание простой формы

Razor Pages предназначена для упрощения реализации типовых шаблонов, которые используются в браузерах, при создании приложения. Привязки модели, вспомогательные функции тегов и вспомогательные методы HTML отлично работают со свойствами, определенными в классе Razor Pages. Рассмотрим страницу с простой формой связи для модели Contact.

В представленных в этой статье примерах DbContext инициализируется в файле Startup.cs.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;

namespace RazorPagesContacts
{
    public class Startup
    {
        public IHostingEnvironment HostingEnvironment { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<AppDbContext>(options =>
                              options.UseInMemoryDatabase("name"));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
    }
}

Модель данных:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Data
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(100)]
        public string Name { get; set; }
    }
}

Контекст базы данных:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
    }
}

Файл представления Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

Модель страницы Pages/Create.cshtml.cs:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
    public class CreateModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }
    }
}

Как правило, класс PageModel называется <PageName>Model и находится в том же пространстве имен, что и страница.

Класс PageModel позволяет разделять логику страницы и ее представление. Он определяет обработчики страницы для запросов, отправляемых на страницу, а также данные для ее визуализации. Это разделение позволяет:

Страница содержит метод обработчика OnPostAsync, который выполняется по запросам POST (когда пользователь публикует форму). Методы обработчика можно добавить для любой HTTP-команды. Наиболее распространенные обработчики

  • OnGet — инициализация необходимого для страницы состояния. Пример обработчика OnGet.
  • OnPost — обработка отправленных через форму данных.

Суффикс Async не является обязательным, но часто используется для асинхронных функций. Этот код типичен для Razor Pages.

Если вы знакомы с приложениями ASP.NET, использующими контроллеры и представления:

  • Код OnPostAsync в предыдущем примере похож на стандартный код контроллера.
  • Большинство примитивов MVC, включая привязку модели, проверку, проверку и результаты действий, являются общими.

Предыдущий метод OnPostAsync:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _db.Customers.Add(Customer);
    await _db.SaveChangesAsync();
    return RedirectToPage("/Index");
}

Простая схема OnPostAsync:

Проверка на наличие ошибок проверки.

  • Если ошибок нет, сохранение данных и перенаправление.
  • Если есть ошибки, отображение страницы с сообщениями проверки. Проверка на стороне клиента выполняется точно так же, как в традиционных приложениях MVC ASP.NET Core. Во многих случаях ошибки проверки выявляются на клиентском компьютере и на сервер не отправляются.

После успешного ввода данных метод обработчика OnPostAsync вызывает метод обработчика RedirectToPage, чтобы получить экземпляр RedirectToPageResult. RedirectToPage — это новый результат действия, аналогичный RedirectToAction или RedirectToRoute, но настроенный для страниц. В приведенном выше примере он выполняет перенаправление на корневую страницу индекса (/Index). Более подробно RedirectToPage рассматривается в разделе Создание URL для страниц.

Если в отправленной форме имеются ошибки проверки (переданные на сервер), метод обработчика OnPostAsync вызывает метод обработчика Page. Page возвращает экземпляр PageResult. Возвращение Page аналогично тому, как действия в контроллерах возвращают View. PageResult — тип возвращаемого значения по умолчанию для метода обработчика. Метод обработчика, вернувший void, визуализирует страницу.

Для указания согласия на привязку модели в свойстве Customer используется атрибут [BindProperty].

public class CreateModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateModel(AppDbContext db)
    {
        _db = db;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        return RedirectToPage("/Index");
    }
}

По умолчанию Razor Pages привязывает свойства ко всем командам, кроме GET. Привязка к свойствам позволяет сократить объем необходимого кода. Привязка уменьшает код за счет того, что для визуализации полей формы (<input asp-for="Customer.Name">) и получения входных данных используется одно и то же свойство.

Предупреждение

В целях безопасности вам следует задать привязку данных запроса GET к свойствам страничной модели. Проверьте введенные данные пользователя, прежде чем сопоставлять их со свойствами. Привязка GET полезна при обращении к сценариям, использующим строку запроса или значения маршрутов.

Чтобы привязать свойство к запросам GET, задайте для свойства SupportsGet атрибута [BindProperty] значение true:

[BindProperty(SupportsGet = true)]

Дополнительные сведения: ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

Домашняя страница (Index.cshtml):

@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customers)
            {
                <tr>
                    <td>@contact.Id</td>
                    <td>@contact.Name</td>
                    <td>
                        <a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
                        <button type="submit" asp-page-handler="delete" 
                                asp-route-id="@contact.Id">delete</button>
                    </td>
                </tr>
            }
        </tbody>
    </table>

    <a asp-page="./Create">Create</a>
</form>

Связанный класс PageModel (Index.cshtml.cs):

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Pages
{
    public class IndexModel : PageModel
    {
        private readonly AppDbContext _db;

        public IndexModel(AppDbContext db)
        {
            _db = db;
        }

        public IList<Customer> Customers { get; private set; }

        public async Task OnGetAsync()
        {
            Customers = await _db.Customers.AsNoTracking().ToListAsync();
        }

        public async Task<IActionResult> OnPostDeleteAsync(int id)
        {
            var contact = await _db.Customers.FindAsync(id);

            if (contact != null)
            {
                _db.Customers.Remove(contact);
                await _db.SaveChangesAsync();
            }

            return RedirectToPage();
        }
    }
}

Файл Index.cshtml содержит следующую разметку для создания ссылки на правку для каждого контактного лица.

<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>

Вспомогательный тег привязки <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> использовал атрибут asp-route-{value} для создания ссылки на страницу редактирования. Эта ссылка содержит данные о маршруте с идентификатором контактного лица. Например, https://localhost:5001/Edit/1. Вспомогательные функции тегов позволяют серверному коду участвовать в создании и отображении HTML-элементов в файлах Razor. Вспомогательные функции тегов включены с помощью @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Файл Pages/Edit.cshtml:

@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@{
    ViewData["Title"] = "Edit Customer";
}

<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name" ></span>
        </div>
    </div>
 
    <div>
        <button type="submit">Save</button>
    </div>
</form>

Первая строка содержит директиву @page "{id:int}". Ограничение маршрутизации "{id:int}" указывает, что страница должна принимать обращенные к ней запросы, которые содержат данные маршрутизации int. Если запрос к странице не содержит данные о маршруте, которые можно конвертировать в int, среда выполнения возвращает ошибку HTTP 404 (не найдено). Чтобы сделать идентификатор необязательным, добавьте ? к ограничению маршрута:

@page "{id:int?}"

Файл Pages/Edit.cshtml.cs:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Customer = await _db.Customers.FindAsync(id);

            if (Customer == null)
            {
                return RedirectToPage("/Index");
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Attach(Customer).State = EntityState.Modified;

            try
            {
                await _db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw new Exception($"Customer {Customer.Id} not found!");
            }

            return RedirectToPage("/Index");
        }
    }
}

Файл Index.cshtml также содержит разметку для создания кнопки удаления у каждого контакта клиента:

<button type="submit" asp-page-handler="delete" 
        asp-route-id="@contact.Id">delete</button>

Во время обработки кнопки удаления в HTML ее formaction включает параметры для следующего:

  • Идентификатор контакта клиента, указанный атрибутом asp-route-id.
  • Параметр handler, указанный атрибутом asp-page-handler.

Пример отображенной кнопки удаления с идентификатором контакта клиента 1:

<button type="submit" formaction="/?id=1&amp;handler=delete">delete</button>

При выборе кнопки на сервер отправляется запрос формы POST. По соглашению имя метода обработчика выбирается на основе значения параметра handler в соответствии со схемой OnPost[handler]Async.

Так как handler — delete в этом примере, метод обработчика OnPostDeleteAsync используется для обработки запроса POST. Если asp-page-handler имеет другое значение, например remove, выбирается метод обработчика с именем OnPostRemoveAsync. В приведенном ниже коде показан обработчик OnPostDeleteAsync:

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _db.Customers.FindAsync(id);

    if (contact != null)
    {
        _db.Customers.Remove(contact);
        await _db.SaveChangesAsync();
    }

    return RedirectToPage();
}

Метод OnPostDeleteAsync:

  • Принимает id из строки запроса. Если директива страницы index.cshtml содержит ограничение маршрутизации "{id:int?}", то id будет получено из данных маршрута. Данные маршрута для id указываются в универсальном коде ресурса (URI), например https://localhost:5001/Customers/2.
  • Отправляет в базу данных запрос контакта клиента с FindAsync.
  • Если контакт клиента найден, он удаляется из списка контактов. База данных обновляется.
  • Вызывает RedirectToPage для перенаправления на корневую страницу индекса (/Index).

Маркировка свойств страницы как обязательных

Свойства класса PageModel можно отметить атрибутом Required:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Pages.Movies
{
    public class CreateModel : PageModel
    {
        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        [Required(ErrorMessage = "Color is required")]
        public string Color { get; set; }

        public IActionResult OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            // Process color.

            return RedirectToPage("./Index");
        }
    }
}

Дополнительные сведения см. в статье Проверка модели.

Обработка запросов HEAD с помощью вызова резервного обработчика OnGet

Запросы HEAD позволяют получать заголовки для определенного ресурса. В отличие от запросов GET запросы HEADне возвращают текст ответа.

Обработчик OnHead обычно создается и вызывается для выполнения запросов HEAD:

public void OnHead()
{
    HttpContext.Response.Headers.Add("HandledBy", "Handled by OnHead!");
}

Если в ASP.NET Core 2.1 или более поздней версии обработчик OnHead не определен, Razor Pages выполнит вызов резервного обработчика OnGet. Это поведение включается путем вызова SetCompatibilityVersion в Startup.ConfigureServices:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

Шаблоны по умолчанию создают вызов SetCompatibilityVersion в ASP.NET Core 2.1 и 2.2. SetCompatibilityVersion задает для параметра AllowMappingHeadRequestsToGetHandler Razor Pages значение true.

Вместо включения всех возможных поведений с помощью метода SetCompatibilityVersion вы можете задать конкретное поведение. Следующий код позволяет сопоставлять запросы HEAD с обработчиком OnGet:

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.AllowMappingHeadRequestsToGetHandler = true;
    });

XSRF/CSRF и Razor Pages

Вам не придется писать отдельный код для проверки подлинности запросов. Razor Pages включает создание и проверку маркеров защиты от подделок по умолчанию.

Использование макетов, частичных реплик, шаблонов и вспомогательных функций тегов с Razor Pages

Pages работает со всеми функциями подсистемы просмотра Razor. Макеты, частичные реплики, шаблоны, вспомогательные функции тегов, а также файлы _ViewStart.cshtml и _ViewImports.cshtml работают точно так же, как и в стандартных представлениях Razor.

Давайте упростим нашу страницу с помощью некоторых из этих функций.

Добавим макет страницы в файл Pages/Shared/_Layout.cshtml.

<!DOCTYPE html>
<html>
<head> 
    <title>Razor Pages Sample</title>      
</head>
<body>    
   <a asp-page="/Index">Home</a>
    @RenderBody()  
    <a asp-page="/Customers/Create">Create</a> <br />
</body>
</html>

Этот макет:

  • управляет макетом каждой страницы (кроме страниц с отказом от макета);
  • импортирует HTML-структуры, такие как JavaScript и таблицы стилей.

Дополнительные сведения см. в статье о макете.

Свойство Layout определяется в файле Pages/_ViewStart.cshtml:

@{
    Layout = "_Layout";
}

Макет хранится в папке Pages/Shared. Pages ищет другие представления (макеты, шаблоны, частичные реплики) в иерархическом порядке, начиная с той папки, где находится текущая страница. Макет в папке Pages/Shared можно использовать на любой странице Razor, которая находится в папке Pages.

Файл макета следует поместить в папку Pages/Shared.

Корпорация Майкрософт рекомендует не размещать файл макета в папке Views/Shared. Views/Shared — это шаблон представлений MVC. Razor Pages опирается на иерархию папок, а не на условные обозначения путей.

Поиск представлений в Razor Pages охватывает папку Pages. Макеты, шаблоны и частичные реплики работают с контроллерами MVC и стандартными представлениями Razor.

Добавим файл Pages/_ViewImports.cshtml:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace описывается далее в этом руководстве. Директива @addTagHelper добавляет встроенные вспомогательные теги на все страницы в папке Pages.

Явное применение директивы @namespace на странице:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

Данная директива задает пространство имен для страницы. Включать пространство имен в директиву @model не требуется.

Если директива @namespace содержится в файле _ViewImports.cshtml, указанное пространство имен определяет префикс для созданного в Pages пространства имен, куда импортируется директива @namespace. Остальная часть созданного пространства имен (суффикс) представляет собой разделенный точками относительный путь между папкой с файлом _ViewImports.cshtml и папкой, содержащей страницу.

Например, класс PageModel в файле Pages/Customers/Edit.cshtml.cs задает пространство имен явно.

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

Файл Pages/_ViewImports.cshtml задает следующее пространство имен:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Сформированное пространство имен для файла Pages/Customers/Edit.cshtml Razor Pages совпадает с пространством имен класса PageModel.

@namespace также работает со стандартными представлениями Razor.

Исходный файл представления Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

Обновленный файл представления Pages/Create.cshtml:

@page
@model CreateModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

Начальный проект Razor Pages содержит файл Pages/_ValidationScriptsPartial.cshtml, который подключает проверку на стороне клиента.

Дополнительные сведения о частичных представлениях см. в Частичные представления в ASP.NET Core.

Формирование URL-адресов для страниц

На представленной выше странице Create используется RedirectToPage:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _db.Customers.Add(Customer);
    await _db.SaveChangesAsync();
    return RedirectToPage("/Index");
}

Это приложение имеет следующую структуру файлов и папок:

  • /Pages

    • Index.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

После успешного выполнения страницы Pages/Customers/Create.cshtml и Pages/Customers/Edit.cshtml перенаправляются на страницу Pages/Index.cshtml. Строка /Index составляет часть URL-адреса для доступа к указанной выше странице. Строка /Index позволяет создавать URL-адреса для страницы Pages/Index.cshtml. Пример:

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">My Index Page</a>
  • RedirectToPage("/Index")

Имя страницы — это путь к странице из корневой папки /Pages, включая начальный символ / (например, /Index). Предыдущие образцы создания URL-адреса обеспечивают расширенные параметры и функциональные возможности по сравнению с жестким заданием URL-адреса. Формирование URL-адресов включает маршрутизацию и позволяет генерировать и включать в код параметры в зависимости от того, как определяется маршрут в пути назначения.

Формирование URL-адресов для страниц поддерживает относительные имена. В приведенной ниже таблице показано, какая страница индекса выбирается с каждым параметром RedirectToPage в Pages/Customers/Create.cshtml.

RedirectToPage(x) Страница
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index"), RedirectToPage("./Index") и RedirectToPage("../Index") — это относительные имена. Для получения имени целевой страницы параметр RedirectToPageкомбинируется с путем текущей страницы.

Привязка относительных имен полезна при создании сайтов со сложной структурой. Если для связи между страницами в определенной папке используются относительные имена, эту папку можно переименовать. При этом все ссылки останутся рабочими (так как не включают имя папки).

Чтобы выполнить перенаправление на страницу в другой области, укажите эту область:

RedirectToPage("/Index", new { area = "Services" });

Для получения дополнительной информации см. Области в ASP.NET Core.

Атрибут ViewData

Данные могут передаваться на страницу с помощью атрибута ViewDataAttribute. Свойства в контроллерах или моделях Razor Page, отмеченные атрибутом [ViewData], обладают собственными значениями, загружаемыми из ViewDataDictionary.

В следующем примере класс AboutModel содержит свойство Title, отмеченное атрибутом [ViewData]. Свойство Title задает заголовок страницы About.

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

На странице About доступ к свойству Title осуществляется как доступ к свойству модели.

<h1>@Model.Title</h1>

В макете заголовок считывается из словаря ViewData.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core позволяет использовать свойство TempData в контроллере. Это свойство хранит данные до тех пор, пока они не будут прочитаны. Для проверки данных без удаления можно использовать методы Keep и Peek. TempData удобно использовать для перенаправления, когда данные требуются больше чем для одного запроса.

В следующем коде значение Message задается с помощью TempData:

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

Представленная ниже разметка в файле Pages/Customers/Index.cshtml отображает значение Message с помощью TempData.

<h3>Msg: @Model.Message</h3>

Модель страницы Pages/Customers/Index.cshtml.cs применяет атрибут [TempData] к свойству Message.

[TempData]
public string Message { get; set; }

Дополнительные сведения см. в разделе TempData.

Несколько обработчиков на страницу

Следующая страница формирует разметку для двух обработчиков с помощью вспомогательной функции тегов asp-page-handler:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

Форма в предыдущем примере включает две кнопки отправки, каждая из которых отправляет данные на отдельный URL-адрес с помощью FormActionTagHelper. Атрибут asp-page-handler является дополнением к asp-page. Атрибут asp-page-handler формирует URL-адреса, ,которые используются для отправки данных в каждый из методов обработчиков, определенных страницей. asp-page не задается, так как пример сопоставлен с текущей страницей.

Модель страницы

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

В представленном выше коде используются именованные методы обработчика. Именованные методы обработчика создаются путем размещения определенного текста в имени после On<HTTP Verb> и перед Async (если есть). В приведенном выше примере использовались методы страницы OnPost JoinList Async и OnPost JoinListUC Async. Если убрать OnPost и Async, имена обработчиков будут выглядеть как JoinList и JoinListUC.

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

При использовании представленного выше кода URL-путь для отправки данных в OnPostJoinListAsync будет выглядеть как https://localhost:5001/Customers/CreateFATH?handler=JoinList. URL-путь для отправки данных в OnPostJoinListUCAsync будет иметь вид https://localhost:5001/Customers/CreateFATH?handler=JoinListUC.

Пользовательские маршруты

С помощью директивы @page можно сделать следующее.

  • Указать пользовательский маршрут к странице. Например, можно задать маршрут к странице "Сведения" /Some/Other/Path: @page "/Some/Other/Path".
  • Добавить сегменты к маршруту страницы по умолчанию. Например, к такому маршруту можно добавить сегмент item: @page "item".
  • Добавить параметры к маршруту страницы по умолчанию. Например, для страницы с @page "{id}" может потребоваться параметр идентификатора id.

Поддерживается путь относительно корня, заданный знаком тильды (~) в начале пути. Например, @page "~/Some/Other/Path" равносильно @page "/Some/Other/Path".

Если вы не хотите, чтобы в URL-адресе отображалась строка запроса ?handler=JoinList, измените маршрут так, чтобы в качестве пути в URL-адресе указывалось имя обработчика. Для настройки маршрута добавьте после директивы @page шаблон маршрута, заключенный в двойные кавычки.

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

При использовании представленного выше кода URL-путь для отправки данных в OnPostJoinListAsync будет выглядеть как https://localhost:5001/Customers/CreateFATH/JoinList. URL-путь для отправки данных в OnPostJoinListUCAsync будет иметь вид https://localhost:5001/Customers/CreateFATH/JoinListUC.

Символ ? после handler означает, что параметр маршрута является необязательным.

Конфигурация и параметры

Чтобы настроить дополнительные параметры, воспользуйтесь методом расширения AddRazorPagesOptions в построителе MVC:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddRazorPagesOptions(options =>
        {
            options.RootDirectory = "/MyPages";
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        });
}

В настоящее время RazorPagesOptions позволяет задавать корневую папку для страниц и добавлять для них обозначения моделей приложений. В дальнейшем мы расширим спектр его возможностей.

Сведения о предварительной компиляции представлений см. на странице Компиляция представлений Razor.

Загрузить или просмотреть пример кода.

Дополнительные общие сведения см. на странице Учебник. Начало работы с Razor Pages в ASP.NET Core.

Указание местонахождения Razor Pages в корне каталога

По умолчанию Razor Pages находится в корне каталога /Pages. Добавьте WithRazorPagesAtContentRoot в AddMvc, чтобы указать, что Razor Pages находится в корневой папке содержимого (ContentRootPath) приложения:

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        ...
    })
    .WithRazorPagesAtContentRoot();

Указание местонахождения Razor Pages в пользовательском корневом каталоге

Добавьте WithRazorPagesRoot в AddMvc, чтобы указать, что Razor Pages находится в пользовательской корневой папке приложения (укажите относительный путь):

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        ...
    })
    .WithRazorPagesRoot("/path/to/razor/pages");

Дополнительные ресурсы