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

Авторы: Рик Андерсон (Rick Anderson), Дейв Брок (Dave Brock) и Кирк Ларкин (Kirk Larkin)

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

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

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

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

  • Visual Studio 2022 с рабочей нагрузкой ASP.NET и веб-разработка.

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

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

Razor Pages

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

В предыдущем коде:

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

@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.

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

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

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;

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

        public DbSet<RazorPagesContacts.Models.Customer> Customer => Set<RazorPagesContacts.Models.Customer>();
    }
}

Файл представления Pages/Customers/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/Customers/Create.cshtml.cs:

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.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();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

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

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

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

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

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

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

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

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

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

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

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

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

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

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

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

Файл представления Pages/Customers/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/Customers/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. В этом примере на клиенте не отображаются ошибки проверки. Обработка ошибок проверки рассматривается далее в этом документе.
    [BindProperty]
    public Customer? Customer { get; set; }
    
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • С ошибками проверки, обнаруженными при проверке на стороне клиента:

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

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

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.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();
        }

        if (Customer != null) _context.Customer.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/Customers/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>
        @if (Model.Customers != null)
        {
            foreach (var contact in Model.Customers)
            {
                <tr>
                    <td> @contact.Id </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

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

public class IndexModel : PageModel
{
    private readonly Data.CustomerDbContext _context;
    public IndexModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

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

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

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

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

        return RedirectToPage();
    }
}

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

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

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

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

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

Отображаемый 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.Customer.FindAsync(id);

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

    return RedirectToPage();
}

Метод OnPostDeleteAsync:

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

Файл Edit.cshtml

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel

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

<h1>Edit</h1>

<h4>Customer</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Customer!.Id" />
            <div class="form-group">
                <label asp-for="Customer!.Name" class="control-label"></label>
                <input asp-for="Customer!.Name" class="form-control" />
                <span asp-validation-for="Customer!.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

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

@page "{id:int?}"

Файл Edit.cshtml.cs:

public class EditModel : PageModel
{
    private readonly RazorPagesContacts.Data.CustomerDbContext _context;

    public EditModel(RazorPagesContacts.Data.CustomerDbContext context)
    {
        _context = context;
    }

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

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Customer = await _context.Customer.FirstOrDefaultAsync(m => m.Id == id);
        
        if (Customer == null)
        {
            return NotFound();
        }
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null)
        {
            _context.Attach(Customer).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CustomerExists(Customer.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
        }

        return RedirectToPage("./Index");
    }

    private bool CustomerExists(int id)
    {
        return _context.Customer.Any(e => e.Id == id);
    }
}

Проверка

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

  • Декларативно задаются в классе 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 на стороне клиента

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

Изоляция CSS

Изоляция стилей CSS для отдельных страниц, представлений и компонентов, чтобы сократить или избежать следующего:

  • Зависимости от глобальных стилей, которые могут быть сложными в обслуживании.
  • Конфликты стилей во вложенном содержимом.

Чтобы добавить CSS-файл с заданной областью для страницы или представления, поместите стили CSS в сопутствующий файл .cshtml.css, имя которого соответствует имени файла .cshtml. В следующем примере файл Index.cshtml.css предоставляет стили CSS, которые применяются только к представлению или странице Index.cshtml.

Pages/Index.cshtml.css (Razor Pages) или Views/Index.cshtml.css (MVC):

h1 {
    color: red;
}

Изоляция CSS выполняется во время сборки. Платформа повторно создает селекторы CSS в соответствии с разметкой, отображаемой на страницах или в представлениях приложения. Созданные повторно стили CSS объединяются и создаются как статический ресурс {APP ASSEMBLY}.styles.css. Заполнитель {APP ASSEMBLY} — это имя сборки проекта. Ссылка на объединенные стили CSS помещается в макет приложения.

В содержимом <head> файла Pages/Shared/_Layout.cshtml приложения (Razor Pages) или Views/Shared/_Layout.cshtml (MVC) приложения добавьте ссылку на объединенные стили CSS или подтвердите ее наличие:

<link rel="stylesheet" href="~/{APP ASSEMBLY}.styles.css" />

В следующем примере имя сборки приложения — WebApp:

<link rel="stylesheet" href="WebApp.styles.css" />

Стили, определенные в файле CSS с заданной областью, применяются только к отображаемым выходным данным соответствующего файла. В предыдущем примере все объявления CSS h1, определенные в других областях приложения, не будут конфликтовать со стилем заголовка Index. Правила каскадов и наследования стилей CSS и дальше действуют для файлов CSS с заданной областью. Например, стили, примененные непосредственно к элементу <h1> в файле Index.cshtml, переопределяют стили файла CSS с заданной областью в Index.cshtml.css.

Примечание

Чтобы обеспечить изоляцию стиля CSS при выполнении объединения, импорт CSS в блоки кода Razor не поддерживается.

Изоляция CSS применяется только к элементам HTML. Изоляция CSS не поддерживается для вспомогательных функций тегов.

В объединенном файле CSS каждая страница, представление или компонент Razor связаны с идентификатором области в формате b-{STRING}, где заполнитель {STRING} представляет собой строку из десяти символов, сгенерированную платформой. В следующем примере представлен стиль для предыдущего элемента <h1> на странице Index приложения Razor Pages:

/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
    color: red;
}

На странице Index, где применяется стиль CSS из объединенного файла, идентификатор области добавляется как атрибут HTML:

<h1 b-3xxtam6d07>

Идентификатор является уникальным для приложения. Во время сборки создается пакет проектов с использованием соглашения {STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css, где заполнитель {STATIC WEB ASSETS BASE PATH} является базовым путем к статическим веб-ресурсам.

Если используются другие проекты, такие как пакеты NuGet или библиотеки классов Razor, то объединенный файл:

  • ссылается на стили с помощью импорта CSS;
  • не публикуется как статический веб-ресурс приложения, использующего стили.

Поддержка препроцессоров CSS

Препроцессоры CSS полезны для улучшения разработки CSS с помощью таких функций, как переменные, вложенность, модули, примеси и наследование. Хотя изоляция CSS изначально не поддерживает препроцессоры CSS, такие как Sass и Less, интеграция препроцессоров CSS происходит без проблем, так как компиляция препроцессора выполняется до того, как платформа перезаписывает селекторы CSS в процессе сборки. С помощью Visual Studio, например, настройте существующую компиляцию препроцессора в качестве задачи перед сборкой в диспетчере выполнения задач Visual Studio.

Многие сторонние пакеты NuGet, такие как Delegate.SassBuilder, могут компилировать файлы SASS и SCSS в начале процесса сборки перед выполнением изоляции CSS, и дополнительная настройка не требуется.

Конфигурация изоляции CSS

Изоляция CSS позволяет настраивать некоторые сложные сценарии, например включающих зависимости от существующих инструментов или рабочих процессов.

Настройка формата идентификатора области

В этом разделе заполнитель {Pages|Views} — либо Pages для приложений Razor Pages, либо Views для приложений MVC.

По умолчанию идентификаторы областей используют формат b-{STRING}, где заполнитель {STRING} — это строка из десяти символов, сгенерированная платформой. Чтобы настроить формат идентификатора области, измените шаблон в файле проекта:

<ItemGroup>
  <None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

В предыдущем примере CSS, сформированный для Index.cshtml.css, изменяет свой идентификатор области с b-{STRING} на custom-scope-identifier.

Используйте идентификаторы областей, чтобы обеспечить наследование файлов CSS с назначенной областью. В следующем примере файла проекта файл BaseView.cshtml.css содержит общие стили для представлений. Файл DerivedView.cshtml.css наследует эти стили.

<ItemGroup>
  <None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-identifier" />
  <None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

Используйте оператор подстановочного знака (*), чтобы предоставить идентификаторы областей для нескольких файлов:

<ItemGroup>
  <None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

Изменение базового пути для статических веб-ресурсов

Файл CSS с заданной областью создается в корне приложения. Чтобы изменить путь по умолчанию, используйте свойство StaticWebAssetBasePath в файле проекта. В следующем примере файл CSS с заданной областью и остальные ресурсы приложения размещаются по пути _content:

<PropertyGroup>
  <StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>

Отключение автоматического объединения

Чтобы отказаться от того, как платформа публикует и загружает файлы с заданной областью во время выполнения, используйте свойство DisableScopedCssBundling. При использовании этого свойства за получение изолированных файлов CSS из каталога obj и их публикацию и загрузку во время выполнения отвечают другие средства или процессы:

<PropertyGroup>
  <DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>

Поддержка библиотеки классов Razor (RCL)

Когда библиотека классов Razor (RCL) предоставляет изолированные стили, атрибут href тега <link> указывает на {STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css со следующими заполнителями:

  • {STATIC WEB ASSET BASE PATH}: базовый путь к статическому веб-ресурсу.
  • {PACKAGE ID}: идентификатор пакета библиотеки. Если идентификатор пакета не указан в файле проекта, идентификатору пакета по умолчанию присваивается имя сборки проекта.

В следующем примере:

  • Базовый путь к статическому веб-ресурсу — _content/ClassLib.
  • Имя сборки библиотеки классов — ClassLib.

Pages/Shared/_Layout.cshtml (Razor Pages) или Views/Shared/_Layout.cshtml (MVC):

<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">

Дополнительные сведения о RCL см. в следующих статьях:

Сведения об изоляции CSS в Blazor см. в статье Изоляция CSS Blazor в ASP.NET Core.

Обработка запросов 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.cshtmlRazor Pages совпадает с пространством имен класса PageModel.

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

Изучите файл представления Pages/Customers/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/Customers/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

    • Privacy.cshtml

    • /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>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </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 (если есть). В приведенном выше примере использовались методы страницы OnPostJoinListAsync и OnPostJoinListUCAsync. Если убрать 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:

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.RootDirectory = "/MyPages";
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
});

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

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

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

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

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

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesAtContentRoot();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

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

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

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesRoot("/path/to/razor/pages");

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

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

Создание проекта 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 отображает страницу RazorCreateModel.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>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </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 содержит следующую разметку:

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

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

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

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

Отображаемый 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.cshtmlRazor 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

    • Privacy.cshtml

    • /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>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </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 (если есть). В приведенном выше примере использовались методы страницы OnPostJoinListAsync и OnPostJoinListUCAsync. Если убрать 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");
}

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