Часть 5. Изменение созданных страниц в приложении ASP.NET Core

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

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

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 8 этой статьи.

Приложение для работы с фильмами подготовлено, но представление данных далеко от идеала. Вместо ReleaseDate должно стоять Дата выпуска, то есть два слова.

Приложение Movie с данными по фильмам, открытое в Chrome

Обновление модели

Обновите Models/Movie.cs следующий выделенный код:

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

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

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

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

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

  • Заметка к данным [Column(TypeName = "decimal(18, 2)")] позволяет Entity Framework Core корректно сопоставить Price с валютой в базе данных. Дополнительные сведения см. в разделе Типы данных.
  • Атрибут [Display] указывает на отображаемое имя поля. В приведенном выше коде Release Date вместо ReleaseDate.
  • Атрибут [DataType] указывает тип данных (Date). Сведения о времени, хранящиеся в поле, не отображаются.

Пространство имен DataAnnotations будет рассмотрено в следующем руководстве.

Перейдите к Pages/Movies и наведите указатель мыши на ссылку Edit (Изменение), чтобы просмотреть целевой URL-адрес.

Окно браузера с указателем, наведенным на ссылку Edit (Изменить), и URL-адресом ссылки https://localhost:1234/Movies/Edit/5

Ссылки Edit, Details и Delete создаются вспомогательной функцией тегов привязки в файле Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

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

В приведенном выше коде Вспомогательная функция привязки тегов динамически создает значение атрибута HTML href на основе Razor Page (маршрут является относительным), атрибут asp-page и идентификатор маршрута (asp-route-id). Дополнительные сведения см. в разделе Формирование URL-адресов для страниц.

Для проверки созданной разметки используйте в браузере параметр Просмотреть исходный код. Ниже показана часть созданного кода HTML:

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

Динамически созданные ссылки передают идентификатор фильма строкой запроса. Например, ?id=1 в https://localhost:5001/Movies/Details?id=1.

Добавление шаблона маршрута

Обновите страницы Razor Pages Edit, Details и Delete так, чтобы использовался шаблон маршрута {id:int}. Измените директиву страницы для каждой из этих страниц c @page на @page "{id:int}". Запустите приложение и просмотрите исходный код.

Созданный код HTML добавляет идентификатор в путь URL-адреса:

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Запрос на страницу с шаблоном {id:int} маршрута, который не включает целое число, возвращает ошибку HTTP 404 (не найдена). Например, https://localhost:5001/Movies/Details возвращает ошибку 404. Чтобы сделать идентификатор необязательным, добавьте ? к ограничению маршрута:

@page "{id:int?}"

Чтобы проверить поведение @page "{id:int?}":

  1. Задайте директиву страницы в Pages/Movies/Details.cshtml как @page "{id:int?}".
  2. Установите точку останова в public async Task<IActionResult> OnGetAsync(int? id), в Pages/Movies/Details.cshtml.cs.
  3. Перейдите к https://localhost:5001/Movies/Details/.

Из-за директивы @page "{id:int}" точка останова не достигается. Механизм маршрутизации возвращает ошибку HTTP 404. При использовании @page "{id:int?}" метод OnGetAsync возвращает NotFound (HTTP 404):

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

    var movie = await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }
    else
    {
        Movie = movie;
    }
    return Page();
}

Проверка обработки исключений нежесткой блокировки

Просмотрите OnPostAsync Pages/Movies/Edit.cshtml.cs метод в файле:

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

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

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

    return RedirectToPage("./Index");
}

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

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

Чтобы протестировать блок catch, выполните указанные ниже действия.

  1. Задайте точку останова в catch (DbUpdateConcurrencyException).
  2. Выберите команду Изменить у фильма, внесите изменения, но не вводите Сохранить.
  3. В другом окне браузера щелкните ссылку Delete для этого же фильма, а затем удалите его.
  4. В первом окне браузера опубликуйте изменения для фильма.

Коду в рабочей среде может потребоваться обнаружение конфликтов параллелизма. Дополнительные сведения см. в статье Обработка конфликтов параллелизма.

Проверка публикации и привязки

Просмотрите файл Pages/Movies/Edit.cshtml.cs.

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

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

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        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();
        }

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

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

        return RedirectToPage("./Index");
    }

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

При выполнении HTTP-запроса GET к странице Movies/Edit, например https://localhost:5001/Movies/Edit/3, происходит следующее:

  • Метод OnGetAsync извлекает запись фильма из базы данных и возвращает метод Page.
  • Метод Page отображает страницу Pages/Movies/Edit.cshtmlRazor. Файл Pages/Movies/Edit.cshtml содержит директиву @model RazorPagesMovie.Pages.Movies.EditModelмодели, которая делает модель фильма доступной на странице.
  • Отображается форма Edit со значениями из записи фильма.

При публикации страницы Movies/Edit происходит следующее:

  • Значения формы на странице привязываются к свойству Movie. Атрибут [BindProperty] обеспечивает привязку модели.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • При наличии ошибок в состоянии модели, например ReleaseDate невозможно преобразовать в дату, форма отображается повторно с предоставленными значениями.

  • Если ошибки модели отсутствуют, данные фильма сохраняются.

Методы HTTP GET на страницах Razor Index, Create и Delete работают аналогично. Метод HTTP POST OnPostAsync на странице Razor Create работает аналогично методу OnPostAsync на странице Razor Edit.

Следующие шаги

Приложение для работы с фильмами подготовлено, но представление данных далеко от идеала. Вместо ReleaseDate должно стоять Дата выпуска, то есть два слова.

Приложение Movie с данными по фильмам, открытое в Chrome

Обновление модели

Обновите Models/Movie.cs следующий выделенный код:

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

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

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

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

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

  • Заметка к данным [Column(TypeName = "decimal(18, 2)")] позволяет Entity Framework Core корректно сопоставить Price с валютой в базе данных. Дополнительные сведения см. в разделе Типы данных.
  • Атрибут [Display] указывает на отображаемое имя поля. В приведенном выше коде Release Date вместо ReleaseDate.
  • Атрибут [DataType] указывает тип данных (Date). Сведения о времени, хранящиеся в поле, не отображаются.

Пространство имен DataAnnotations будет рассмотрено в следующем руководстве.

Перейдите к Pages/Movies и наведите указатель мыши на ссылку Edit (Изменение), чтобы просмотреть целевой URL-адрес.

Окно браузера с указателем, наведенным на ссылку Edit (Изменить), и URL-адресом ссылки https://localhost:1234/Movies/Edit/5

Ссылки Edit, Details и Delete создаются вспомогательной функцией тегов привязки в файле Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

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

В приведенном выше коде Вспомогательная функция привязки тегов динамически создает значение атрибута HTML href на основе Razor Page (маршрут является относительным), атрибут asp-page и идентификатор маршрута (asp-route-id). Дополнительные сведения см. в разделе Формирование URL-адресов для страниц.

Для проверки созданной разметки используйте в браузере параметр Просмотреть исходный код. Ниже показана часть созданного кода HTML:

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

Динамически созданные ссылки передают идентификатор фильма строкой запроса. Например, ?id=1 в https://localhost:5001/Movies/Details?id=1.

Добавление шаблона маршрута

Обновите страницы Razor Pages Edit, Details и Delete так, чтобы использовался шаблон маршрута {id:int}. Измените директиву страницы для каждой из этих страниц c @page на @page "{id:int}". Запустите приложение и просмотрите исходный код.

Созданный код HTML добавляет идентификатор в путь URL-адреса:

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Запрос на страницу с шаблоном {id:int} маршрута, который не включает целое число, возвращает ошибку HTTP 404 (не найдена). Например, https://localhost:5001/Movies/Details возвращает ошибку 404. Чтобы сделать идентификатор необязательным, добавьте ? к ограничению маршрута:

@page "{id:int?}"

Чтобы проверить поведение @page "{id:int?}":

  1. Задайте директиву страницы в Pages/Movies/Details.cshtml как @page "{id:int?}".
  2. Установите точку останова в public async Task<IActionResult> OnGetAsync(int? id), в Pages/Movies/Details.cshtml.cs.
  3. Перейдите к https://localhost:5001/Movies/Details/.

Из-за директивы @page "{id:int}" точка останова не достигается. Механизм маршрутизации возвращает ошибку HTTP 404. При использовании @page "{id:int?}" метод OnGetAsync возвращает NotFound (HTTP 404):

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

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Проверка обработки исключений нежесткой блокировки

Просмотрите OnPostAsync Pages/Movies/Edit.cshtml.cs метод в файле:

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

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

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

    return RedirectToPage("./Index");
}

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

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

Чтобы протестировать блок catch, выполните указанные ниже действия.

  1. Задайте точку останова в catch (DbUpdateConcurrencyException).
  2. Выберите команду Изменить у фильма, внесите изменения, но не вводите Сохранить.
  3. В другом окне браузера щелкните ссылку Delete для этого же фильма, а затем удалите его.
  4. В первом окне браузера опубликуйте изменения для фильма.

Коду в рабочей среде может потребоваться обнаружение конфликтов параллелизма. Дополнительные сведения см. в статье Обработка конфликтов параллелизма.

Проверка публикации и привязки

Просмотрите файл Pages/Movies/Edit.cshtml.cs.

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

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

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        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();
        }

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

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

        return RedirectToPage("./Index");
    }

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

При выполнении HTTP-запроса GET к странице Movies/Edit, например https://localhost:5001/Movies/Edit/3, происходит следующее:

  • Метод OnGetAsync извлекает запись фильма из базы данных и возвращает метод Page.
  • Метод Page отображает страницу Pages/Movies/Edit.cshtmlRazor. Файл Pages/Movies/Edit.cshtml содержит директиву @model RazorPagesMovie.Pages.Movies.EditModelмодели, которая делает модель фильма доступной на странице.
  • Отображается форма Edit со значениями из записи фильма.

При публикации страницы Movies/Edit происходит следующее:

  • Значения формы на странице привязываются к свойству Movie. Атрибут [BindProperty] обеспечивает привязку модели.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • При наличии ошибок в состоянии модели, например ReleaseDate невозможно преобразовать в дату, форма отображается повторно с предоставленными значениями.

  • Если ошибки модели отсутствуют, данные фильма сохраняются.

Методы HTTP GET на страницах Razor Index, Create и Delete работают аналогично. Метод HTTP POST OnPostAsync на странице Razor Create работает аналогично методу OnPostAsync на странице Razor Edit.

Следующие шаги

Приложение для работы с фильмами подготовлено, но представление данных далеко от идеала. Вместо ReleaseDate должно стоять Дата выпуска, то есть два слова.

Приложение Movie с данными по фильмам, открытое в Chrome

Обновление модели

Обновите Models/Movie.cs следующий выделенный код:

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

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

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

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

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

  • Заметка к данным [Column(TypeName = "decimal(18, 2)")] позволяет Entity Framework Core корректно сопоставить Price с валютой в базе данных. Дополнительные сведения см. в разделе Типы данных.
  • Атрибут [Display] указывает на отображаемое имя поля. В приведенном выше коде Release Date вместо ReleaseDate.
  • Атрибут [DataType] указывает тип данных (Date). Сведения о времени, хранящиеся в поле, не отображаются.

Пространство имен DataAnnotations будет рассмотрено в следующем руководстве.

Перейдите к Pages/Movies и наведите указатель мыши на ссылку Edit (Изменение), чтобы просмотреть целевой URL-адрес.

Окно браузера с указателем, наведенным на ссылку Edit (Изменить), и URL-адресом ссылки https://localhost:1234/Movies/Edit/5

Ссылки Edit, Details и Delete создаются вспомогательной функцией тегов привязки в файле Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

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

В приведенном выше коде Вспомогательная функция привязки тегов динамически создает значение атрибута HTML href на основе Razor Page (маршрут является относительным), атрибут asp-page и идентификатор маршрута (asp-route-id). Дополнительные сведения см. в разделе Формирование URL-адресов для страниц.

Для проверки созданной разметки используйте в браузере параметр Просмотреть исходный код. Ниже показана часть созданного кода HTML:

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

Динамически созданные ссылки передают идентификатор фильма строкой запроса. Например, ?id=1 в https://localhost:5001/Movies/Details?id=1.

Добавление шаблона маршрута

Обновите страницы Razor Pages Edit, Details и Delete так, чтобы использовался шаблон маршрута {id:int}. Измените директиву страницы для каждой из этих страниц c @page на @page "{id:int}". Запустите приложение и просмотрите исходный код.

Созданный код HTML добавляет идентификатор в путь URL-адреса:

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Запрос на страницу с шаблоном {id:int} маршрута, который не включает целое число, возвращает ошибку HTTP 404 (не найдена). Например, https://localhost:5001/Movies/Details возвращает ошибку 404. Чтобы сделать идентификатор необязательным, добавьте ? к ограничению маршрута:

@page "{id:int?}"

Чтобы проверить поведение @page "{id:int?}":

  1. Задайте директиву страницы в Pages/Movies/Details.cshtml как @page "{id:int?}".
  2. Установите точку останова в public async Task<IActionResult> OnGetAsync(int? id), в Pages/Movies/Details.cshtml.cs.
  3. Перейдите к https://localhost:5001/Movies/Details/.

Из-за директивы @page "{id:int}" точка останова не достигается. Механизм маршрутизации возвращает ошибку HTTP 404. При использовании @page "{id:int?}" метод OnGetAsync возвращает NotFound (HTTP 404):

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

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Проверка обработки исключений нежесткой блокировки

Просмотрите OnPostAsync Pages/Movies/Edit.cshtml.cs метод в файле:

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

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

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

    return RedirectToPage("./Index");
}

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

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

Чтобы протестировать блок catch, выполните указанные ниже действия.

  1. Задайте точку останова в catch (DbUpdateConcurrencyException).
  2. Выберите команду Изменить у фильма, внесите изменения, но не вводите Сохранить.
  3. В другом окне браузера щелкните ссылку Delete для этого же фильма, а затем удалите его.
  4. В первом окне браузера опубликуйте изменения для фильма.

Коду в рабочей среде может потребоваться обнаружение конфликтов параллелизма. Дополнительные сведения см. в статье Обработка конфликтов параллелизма.

Проверка публикации и привязки

Просмотрите файл Pages/Movies/Edit.cshtml.cs.

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

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

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        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();
        }

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

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

        return RedirectToPage("./Index");
    }

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

При выполнении HTTP-запроса GET к странице Movies/Edit, например https://localhost:5001/Movies/Edit/3, происходит следующее:

  • Метод OnGetAsync извлекает запись фильма из базы данных и возвращает метод Page.
  • Метод Page отображает страницу Pages/Movies/Edit.cshtmlRazor. Файл Pages/Movies/Edit.cshtml содержит директиву @model RazorPagesMovie.Pages.Movies.EditModelмодели, которая делает модель фильма доступной на странице.
  • Отображается форма Edit со значениями из записи фильма.

При публикации страницы Movies/Edit происходит следующее:

  • Значения формы на странице привязываются к свойству Movie. Атрибут [BindProperty] обеспечивает привязку модели.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • При наличии ошибок в состоянии модели, например ReleaseDate невозможно преобразовать в дату, форма отображается повторно с предоставленными значениями.

  • Если ошибки модели отсутствуют, данные фильма сохраняются.

Методы HTTP GET на страницах Razor Index, Create и Delete работают аналогично. Метод HTTP POST OnPostAsync на странице Razor Create работает аналогично методу OnPostAsync на странице Razor Edit.

Следующие шаги

Приложение для работы с фильмами подготовлено, но представление данных далеко от идеала. Вместо ReleaseDate должно стоять Дата выпуска, то есть два слова.

Приложение Movie с данными по фильмам, открытое в Chrome

Обновление созданного кода

Обновите Models/Movie.cs следующий выделенный код:

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

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; } = string.Empty;

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

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}

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

  • Заметка к данным [Column(TypeName = "decimal(18, 2)")] позволяет Entity Framework Core корректно сопоставить Price с валютой в базе данных. Дополнительные сведения см. в разделе Типы данных.
  • Атрибут [Display] указывает на отображаемое имя поля. В приведенном выше коде ReleaseDate заменен на "Дата выпуска".
  • Атрибут [DataType] указывает тип данных (Date). Сведения о времени, хранящиеся в поле, не отображаются.

Пространство имен DataAnnotations будет рассмотрено в следующем руководстве.

Перейдите к Pages/Movies и наведите указатель мыши на ссылку Edit (Изменение), чтобы просмотреть целевой URL-адрес.

Окно браузера с указателем, наведенным на ссылку Edit (Изменить), и URL-адресом ссылки https://localhost:1234/Movies/Edit/5

Ссылки Edit, Details и Delete создаются вспомогательной функцией тегов привязки в файле Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

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

В приведенном выше коде Вспомогательная функция привязки тегов динамически создает значение атрибута HTML href на основе Razor Page (маршрут является относительным), атрибут asp-page и идентификатор маршрута (asp-route-id). Дополнительные сведения см. в разделе Формирование URL-адресов для страниц.

Для проверки созданной разметки используйте в браузере параметр Просмотреть исходный код. Ниже показана часть созданного кода HTML:

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

В динамически созданных ссылках идентификаторы фильмов передаются с помощью строки запроса. Например, ?id=1 в https://localhost:5001/Movies/Details?id=1.

Добавление шаблона маршрута

Обновите страницы Razor Pages Edit, Details и Delete так, чтобы использовался шаблон маршрута {id:int}. Измените директиву страницы для каждой из этих страниц c @page на @page "{id:int}". Запустите приложение и просмотрите исходный код.

Созданный код HTML добавляет идентификатор в путь URL-адреса:

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Запрос к странице с шаблоном маршрута {id:int}, который не включает в себя целое число, приводит к ошибке HTTP 404 (не найдено). Например, https://localhost:5001/Movies/Details приведет к ошибке 404. Чтобы сделать идентификатор необязательным, добавьте ? к ограничению маршрута:

@page "{id:int?}"

Чтобы проверить поведение @page "{id:int?}":

  1. Задайте директиву страницы в Pages/Movies/Details.cshtml как @page "{id:int?}".
  2. Установите точку останова в public async Task<IActionResult> OnGetAsync(int? id), в Pages/Movies/Details.cshtml.cs.
  3. Перейдите к https://localhost:5001/Movies/Details/.

Из-за директивы @page "{id:int}" точка останова не достигается. Механизм маршрутизации возвращает ошибку HTTP 404. При использовании @page "{id:int?}" метод OnGetAsync возвращает NotFound (HTTP 404):

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

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Проверка обработки исключений нежесткой блокировки

Просмотрите OnPostAsync Pages/Movies/Edit.cshtml.cs метод в файле:

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

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

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.ID))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
  return (_context.Movie?.Any(e => e.ID == id)).GetValueOrDefault();
}

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

Чтобы протестировать блок catch, выполните указанные ниже действия.

  1. Задайте точку останова в catch (DbUpdateConcurrencyException).
  2. Выберите команду Изменить у фильма, внесите изменения, но не вводите Сохранить.
  3. В другом окне браузера щелкните ссылку Delete для этого же фильма, а затем удалите его.
  4. В первом окне браузера опубликуйте изменения для фильма.

Рабочий код может потребоваться обнаружить дополнительные конфликты параллелизма, такие как несколько клиентов, изменяя сущность одновременно. Дополнительные сведения см. в статье Обработка конфликтов параллелизма.

Проверка публикации и привязки

Просмотрите файл Pages/Movies/Edit.cshtml.cs.

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

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

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        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();
        }

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

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
      return (_context.Movie?.Any(e => e.ID == id)).GetValueOrDefault();
    }

При выполнении HTTP-запроса GET к странице Movies/Edit, например https://localhost:5001/Movies/Edit/3, происходит следующее:

  • Метод OnGetAsync извлекает запись фильма из базы данных и возвращает метод Page.
  • Метод Page отображает страницу Pages/Movies/Edit.cshtmlRazor. Файл Pages/Movies/Edit.cshtml содержит директиву @model RazorPagesMovie.Pages.Movies.EditModelмодели, которая делает модель фильма доступной на странице.
  • Отображается форма Edit со значениями из записи фильма.

При публикации страницы Movies/Edit происходит следующее:

  • Значения формы на странице привязываются к свойству Movie. Атрибут [BindProperty] обеспечивает привязку модели.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • При наличии ошибок в состоянии модели, например ReleaseDate невозможно преобразовать в дату, форма отображается повторно с предоставленными значениями.

  • Если ошибки модели отсутствуют, данные фильма сохраняются.

Методы HTTP GET на страницах Razor Index, Create и Delete работают аналогично. Метод HTTP POST OnPostAsync на странице Razor Create работает аналогично методу OnPostAsync на странице Razor Edit.

Следующие шаги

Приложение для работы с фильмами подготовлено, но представление данных далеко от идеала. Вместо ReleaseDate должно стоять Дата выпуска, то есть два слова.

Приложение Movie с данными по фильмам, открытое в Chrome

Обновление созданного кода

Откройте файл и добавьте выделенные Models/Movie.cs строки, показанные в следующем коде:

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

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

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

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}

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

  • Заметка к данным [Column(TypeName = "decimal(18, 2)")] позволяет Entity Framework Core корректно сопоставить Price с валютой в базе данных. Дополнительные сведения см. в разделе Типы данных.
  • Атрибут [Display] указывает на отображаемое имя поля. В приведенном выше коде ReleaseDate заменен на "Дата выпуска".
  • Атрибут [DataType] указывает тип данных (Date). Сведения о времени, хранящиеся в поле, не отображаются.

Пространство имен DataAnnotations будет рассмотрено в следующем руководстве.

Перейдите к Pages/Movies и наведите указатель мыши на ссылку Edit (Изменение), чтобы просмотреть целевой URL-адрес.

Окно браузера с указателем, наведенным на ссылку Edit (Изменить), и URL-адресом ссылки https://localhost:1234/Movies/Edit/5

Ссылки Edit, Details и Delete создаются вспомогательной функцией тегов привязки в файле Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

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

В приведенном выше коде Вспомогательная функция привязки тегов динамически создает значение атрибута HTML href на основе Razor Page (маршрут является относительным), атрибут asp-page и идентификатор маршрута (asp-route-id). Дополнительные сведения см. в разделе Формирование URL-адресов для страниц.

Для проверки созданной разметки используйте в браузере параметр Просмотреть исходный код. Ниже показана часть созданного кода HTML:

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

В динамически созданных ссылках идентификаторы фильмов передаются с помощью строки запроса. Например, ?id=1 в https://localhost:5001/Movies/Details?id=1.

Добавление шаблона маршрута

Обновите страницы Razor Pages Edit, Details и Delete так, чтобы использовался шаблон маршрута {id:int}. Измените директиву страницы для каждой из этих страниц c @page на @page "{id:int}". Запустите приложение и просмотрите исходный код.

Созданный код HTML добавляет идентификатор в путь URL-адреса:

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Запрос к странице с шаблоном маршрута {id:int}, который не включает в себя целое число, приводит к ошибке HTTP 404 (не найдено). Например, https://localhost:5001/Movies/Details приведет к ошибке 404. Чтобы сделать идентификатор необязательным, добавьте ? к ограничению маршрута:

@page "{id:int?}"

Чтобы проверить поведение @page "{id:int?}":

  1. Задайте директиву страницы в Pages/Movies/Details.cshtml как @page "{id:int?}".
  2. Установите точку останова в public async Task<IActionResult> OnGetAsync(int? id), в Pages/Movies/Details.cshtml.cs.
  3. Перейдите к https://localhost:5001/Movies/Details/.

Из-за директивы @page "{id:int}" точка останова не достигается. Механизм маршрутизации возвращает ошибку HTTP 404. При использовании @page "{id:int?}" метод OnGetAsync возвращает NotFound (HTTP 404):

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

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Проверка обработки исключений нежесткой блокировки

Просмотрите OnPostAsync Pages/Movies/Edit.cshtml.cs метод в файле:

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

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

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.ID))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
    return _context.Movie.Any(e => e.ID == id);
}

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

Чтобы протестировать блок catch, выполните указанные ниже действия.

  1. Задайте точку останова в catch (DbUpdateConcurrencyException).
  2. Выберите команду Изменить у фильма, внесите изменения, но не вводите Сохранить.
  3. В другом окне браузера щелкните ссылку Delete для этого же фильма, а затем удалите его.
  4. В первом окне браузера опубликуйте изменения для фильма.

Коду в рабочей среде может потребоваться обнаружение конфликтов параллелизма. Дополнительные сведения см. в статье Обработка конфликтов параллелизма.

Проверка публикации и привязки

Просмотрите файл Pages/Movies/Edit.cshtml.cs.

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

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

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

        Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

        if (Movie == null)
        {
            return NotFound();
        }
        return Page();
    }

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

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

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
        return _context.Movie.Any(e => e.ID == id);
    }

При выполнении HTTP-запроса GET к странице Movies/Edit, например https://localhost:5001/Movies/Edit/3, происходит следующее:

  • Метод OnGetAsync извлекает запись фильма из базы данных и возвращает метод Page.
  • Метод Page отображает страницу Pages/Movies/Edit.cshtmlRazor. Файл Pages/Movies/Edit.cshtml содержит директиву @model RazorPagesMovie.Pages.Movies.EditModelмодели, которая делает модель фильма доступной на странице.
  • Отображается форма Edit со значениями из записи фильма.

При публикации страницы Movies/Edit происходит следующее:

  • Значения формы на странице привязываются к свойству Movie. Атрибут [BindProperty] обеспечивает привязку модели.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • При наличии ошибок в состоянии модели, например ReleaseDate невозможно преобразовать в дату, форма отображается повторно с предоставленными значениями.

  • Если ошибки модели отсутствуют, данные фильма сохраняются.

Методы HTTP GET на страницах Razor Index, Create и Delete работают аналогично. Метод HTTP POST OnPostAsync на странице Razor Create работает аналогично методу OnPostAsync на странице Razor Edit.

Следующие шаги