第 5 部分,在 ASP.NET Core 应用中更新生成的页面

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

构架的电影应用有个不错的开始,但是展示效果还不够理想。 ReleaseDate 应是两个词 (Release Date)。

在 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,并将鼠标悬停在“编辑”链接上以查看目标 URL。

显示鼠标悬停在“编辑”链接上的浏览器窗口,显示了 https://localhost:1234/Movies/Edit/5 的链接 URL

“编辑”、“详细信息”和“删除”链接是在 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>

标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。

在前面的代码中,定位标记帮助程序从 Razor 页面(路由是相对的)、asp-page 和路由标识符 (asp-route-id) 动态生成 HTML href 特性值。 有关详细信息,请参阅页面的 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。 例如,https://localhost:5001/Movies/Details?id=1 中的 ?id=1

添加路由模板

更新“编辑”、“详细信息”和“删除”Razor 页面以使用 {id:int} 路由模板。 将上述每个页面的页面指令从 @page 更改为 @page "{id:int}"。 运行应用,然后查看源。

生成的 HTML 会将 ID 添加到 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 错误。 若要使 ID 可选,请将 ? 追加到路由约束:

@page "{id:int?}"

测试 @page "{id:int?}" 的行为:

  1. Pages/Movies/Details.cshtml 中将 page 指令设置为 @page "{id:int?}"
  2. Pages/Movies/Details.cshtml.cs 中的 public async Task<IActionResult> OnGetAsync(int? id) 中设置断点。
  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();
}

查看并发异常处理

查看 Pages/Movies/Edit.cshtml.cs 文件中的 OnPostAsync 方法:

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. 在其他浏览器窗口中,选择同一电影的“删除”链接,然后删除此电影。
  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);
    }

当对 Movies/Edit 页面(例如 https://localhost:5001/Movies/Edit/3)进行 HTTP GET 请求时:

  • OnGetAsync 方法从数据库提取电影并返回 Page 方法。
  • Page 方法呈现 Pages/Movies/Edit.cshtmlRazor 页面。 Pages/Movies/Edit.cshtml 文件包含模型指令 @model RazorPagesMovie.Pages.Movies.EditModel,这使电影模型在页面上可用。
  • “编辑”表单中会显示电影的值。

当发布 Movies/Edit 页面时:

  • 此页面上的表单值将绑定到 Movie 属性。 [BindProperty] 特性会启用模型绑定

    [BindProperty]
    public Movie Movie { get; set; }
    
  • 如果模型状态中存在错误(例如,ReleaseDate 无法被转换为日期),则会使用已提交的值重新显示表单。

  • 如果没有模型错误,则电影已保存。

“索引”、“创建”和“删除”Razor 页面中的 HTTP GET 方法遵循一个类似的模式。 “创建”Razor 页面中的 HTTP POST OnPostAsync 方法遵循的模式类似于“编辑”Razor 页面中的 OnPostAsync 方法所遵循的模式。

后续步骤

构架的电影应用有个不错的开始,但是展示效果还不够理想。 ReleaseDate 应是两个词 (Release Date)。

在 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,并将鼠标悬停在“编辑”链接上以查看目标 URL。

显示鼠标悬停在“编辑”链接上的浏览器窗口,显示了 https://localhost:1234/Movies/Edit/5 的链接 URL

“编辑”、“详细信息”和“删除”链接是在 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>

标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。

在前面的代码中,定位标记帮助程序从 Razor 页面(路由是相对的)、asp-page 和路由标识符 (asp-route-id) 动态生成 HTML href 特性值。 有关详细信息,请参阅页面的 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。 例如,https://localhost:5001/Movies/Details?id=1 中的 ?id=1

添加路由模板

更新“编辑”、“详细信息”和“删除”Razor 页面以使用 {id:int} 路由模板。 将上述每个页面的页面指令从 @page 更改为 @page "{id:int}"。 运行应用,然后查看源。

生成的 HTML 会将 ID 添加到 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 错误。 若要使 ID 可选,请将 ? 追加到路由约束:

@page "{id:int?}"

测试 @page "{id:int?}" 的行为:

  1. Pages/Movies/Details.cshtml 中将 page 指令设置为 @page "{id:int?}"
  2. Pages/Movies/Details.cshtml.cs 中的 public async Task<IActionResult> OnGetAsync(int? id) 中设置断点。
  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();
}

查看并发异常处理

查看 Pages/Movies/Edit.cshtml.cs 文件中的 OnPostAsync 方法:

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. 在其他浏览器窗口中,选择同一电影的“删除”链接,然后删除此电影。
  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);
    }

当对 Movies/Edit 页面(例如 https://localhost:5001/Movies/Edit/3)进行 HTTP GET 请求时:

  • OnGetAsync 方法从数据库提取电影并返回 Page 方法。
  • Page 方法呈现 Pages/Movies/Edit.cshtmlRazor 页面。 Pages/Movies/Edit.cshtml 文件包含模型指令 @model RazorPagesMovie.Pages.Movies.EditModel,这使电影模型在页面上可用。
  • “编辑”表单中会显示电影的值。

当发布 Movies/Edit 页面时:

  • 此页面上的表单值将绑定到 Movie 属性。 [BindProperty] 特性会启用模型绑定

    [BindProperty]
    public Movie Movie { get; set; }
    
  • 如果模型状态中存在错误(例如,ReleaseDate 无法被转换为日期),则会使用已提交的值重新显示表单。

  • 如果没有模型错误,则电影已保存。

“索引”、“创建”和“删除”Razor 页面中的 HTTP GET 方法遵循一个类似的模式。 “创建”Razor 页面中的 HTTP POST OnPostAsync 方法遵循的模式类似于“编辑”Razor 页面中的 OnPostAsync 方法所遵循的模式。

后续步骤

构架的电影应用有个不错的开始,但是展示效果还不够理想。 ReleaseDate 应是两个词 (Release Date)。

在 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,并将鼠标悬停在“编辑”链接上以查看目标 URL。

显示鼠标悬停在“编辑”链接上的浏览器窗口,显示了 https://localhost:1234/Movies/Edit/5 的链接 URL

“编辑”、“详细信息”和“删除”链接是在 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>

标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。

在前面的代码中,定位标记帮助程序从 Razor 页面(路由是相对的)、asp-page 和路由标识符 (asp-route-id) 动态生成 HTML href 特性值。 有关详细信息,请参阅页面的 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。 例如,https://localhost:5001/Movies/Details?id=1 中的 ?id=1

添加路由模板

更新“编辑”、“详细信息”和“删除”Razor 页面以使用 {id:int} 路由模板。 将上述每个页面的页面指令从 @page 更改为 @page "{id:int}"。 运行应用,然后查看源。

生成的 HTML 会将 ID 添加到 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 错误。 若要使 ID 可选,请将 ? 追加到路由约束:

@page "{id:int?}"

测试 @page "{id:int?}" 的行为:

  1. Pages/Movies/Details.cshtml 中将 page 指令设置为 @page "{id:int?}"
  2. Pages/Movies/Details.cshtml.cs 中的 public async Task<IActionResult> OnGetAsync(int? id) 中设置断点。
  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();
}

查看并发异常处理

查看 Pages/Movies/Edit.cshtml.cs 文件中的 OnPostAsync 方法:

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. 在其他浏览器窗口中,选择同一电影的“删除”链接,然后删除此电影。
  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);
    }

当对 Movies/Edit 页面(例如 https://localhost:5001/Movies/Edit/3)进行 HTTP GET 请求时:

  • OnGetAsync 方法从数据库提取电影并返回 Page 方法。
  • Page 方法呈现 Pages/Movies/Edit.cshtmlRazor 页面。 Pages/Movies/Edit.cshtml 文件包含模型指令 @model RazorPagesMovie.Pages.Movies.EditModel,这使电影模型在页面上可用。
  • “编辑”表单中会显示电影的值。

当发布 Movies/Edit 页面时:

  • 此页面上的表单值将绑定到 Movie 属性。 [BindProperty] 特性会启用模型绑定

    [BindProperty]
    public Movie Movie { get; set; }
    
  • 如果模型状态中存在错误(例如,ReleaseDate 无法被转换为日期),则会使用已提交的值重新显示表单。

  • 如果没有模型错误,则电影已保存。

“索引”、“创建”和“删除”Razor 页面中的 HTTP GET 方法遵循一个类似的模式。 “创建”Razor 页面中的 HTTP POST OnPostAsync 方法遵循的模式类似于“编辑”Razor 页面中的 OnPostAsync 方法所遵循的模式。

后续步骤

构架的电影应用有个不错的开始,但是展示效果还不够理想。 ReleaseDate 应是两个词 (Release Date)。

在 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] 属性指定字段的显示名称。 在前面的代码中即“Release Date”,而非“ReleaseDate”。
  • [DataType] 属性指定数据的类型 (Date)。 此字段中存储的时间信息不显示。

DataAnnotations 未包括在下一个教程中。

浏览到 Pages/Movies,并将鼠标悬停在“编辑”链接上以查看目标 URL。

显示鼠标悬停在“编辑”链接上的浏览器窗口,显示了 https://localhost:1234/Movies/Edit/5 的链接 URL

“编辑”、“详细信息”和“删除”链接是在 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>

标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。

在前面的代码中,定位标记帮助程序从 Razor 页面(路由是相对的)、asp-page 和路由标识符 (asp-route-id) 动态生成 HTML href 特性值。 有关详细信息,请参阅页面的 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。 例如,https://localhost:5001/Movies/Details?id=1 中的 ?id=1

添加路由模板

更新“编辑”、“详细信息”和“删除”Razor 页面以使用 {id:int} 路由模板。 将上述每个页面的页面指令从 @page 更改为 @page "{id:int}"。 运行应用,然后查看源。

生成的 HTML 会将 ID 添加到 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 错误。 若要使 ID 可选,请将 ? 追加到路由约束:

@page "{id:int?}"

测试 @page "{id:int?}" 的行为:

  1. Pages/Movies/Details.cshtml 中将 page 指令设置为 @page "{id:int?}"
  2. Pages/Movies/Details.cshtml.cs 中的 public async Task<IActionResult> OnGetAsync(int? id) 中设置断点。
  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();
}

查看并发异常处理

查看 Pages/Movies/Edit.cshtml.cs 文件中的 OnPostAsync 方法:

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. 在其他浏览器窗口中,选择同一电影的“删除”链接,然后删除此电影。
  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();
    }

当对 Movies/Edit 页面(例如 https://localhost:5001/Movies/Edit/3)进行 HTTP GET 请求时:

  • OnGetAsync 方法从数据库提取电影并返回 Page 方法。
  • Page 方法呈现 Pages/Movies/Edit.cshtmlRazor 页面。 Pages/Movies/Edit.cshtml 文件包含模型指令 @model RazorPagesMovie.Pages.Movies.EditModel,这使电影模型在页面上可用。
  • “编辑”表单中会显示电影的值。

当发布 Movies/Edit 页面时:

  • 此页面上的表单值将绑定到 Movie 属性。 [BindProperty] 特性会启用模型绑定

    [BindProperty]
    public Movie Movie { get; set; }
    
  • 如果模型状态中存在错误(例如,ReleaseDate 无法被转换为日期),则会使用已提交的值重新显示表单。

  • 如果没有模型错误,则电影已保存。

“索引”、“创建”和“删除”Razor 页面中的 HTTP GET 方法遵循一个类似的模式。 “创建”Razor 页面中的 HTTP POST OnPostAsync 方法遵循的模式类似于“编辑”Razor 页面中的 OnPostAsync 方法所遵循的模式。

后续步骤

构架的电影应用有个不错的开始,但是展示效果还不够理想。 ReleaseDate 应是两个词 (Release Date)。

在 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] 属性指定字段的显示名称。 在前面的代码中即“Release Date”,而非“ReleaseDate”。
  • [DataType] 属性指定数据的类型 (Date)。 此字段中存储的时间信息不显示。

DataAnnotations 未包括在下一个教程中。

浏览到 Pages/Movies,并将鼠标悬停在“编辑”链接上以查看目标 URL。

显示鼠标悬停在“编辑”链接上的浏览器窗口,显示了 https://localhost:1234/Movies/Edit/5 的链接 URL

“编辑”、“详细信息”和“删除”链接是在 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>

标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。

在前面的代码中,定位标记帮助程序从 Razor 页面(路由是相对的)、asp-page 和路由标识符 (asp-route-id) 动态生成 HTML href 特性值。 有关详细信息,请参阅页面的 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。 例如,https://localhost:5001/Movies/Details?id=1 中的 ?id=1

添加路由模板

更新“编辑”、“详细信息”和“删除”Razor 页面以使用 {id:int} 路由模板。 将上述每个页面的页面指令从 @page 更改为 @page "{id:int}"。 运行应用,然后查看源。

生成的 HTML 会将 ID 添加到 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 错误。 若要使 ID 可选,请将 ? 追加到路由约束:

@page "{id:int?}"

测试 @page "{id:int?}" 的行为:

  1. Pages/Movies/Details.cshtml 中将 page 指令设置为 @page "{id:int?}"
  2. Pages/Movies/Details.cshtml.cs 中的 public async Task<IActionResult> OnGetAsync(int? id) 中设置断点。
  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();
}

查看并发异常处理

查看 Pages/Movies/Edit.cshtml.cs 文件中的 OnPostAsync 方法:

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. 在其他浏览器窗口中,选择同一电影的“删除”链接,然后删除此电影。
  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);
    }

当对 Movies/Edit 页面(例如 https://localhost:5001/Movies/Edit/3)进行 HTTP GET 请求时:

  • OnGetAsync 方法从数据库提取电影并返回 Page 方法。
  • Page 方法呈现 Pages/Movies/Edit.cshtmlRazor 页面。 Pages/Movies/Edit.cshtml 文件包含模型指令 @model RazorPagesMovie.Pages.Movies.EditModel,这使电影模型在页面上可用。
  • “编辑”表单中会显示电影的值。

当发布 Movies/Edit 页面时:

  • 此页面上的表单值将绑定到 Movie 属性。 [BindProperty] 特性会启用模型绑定

    [BindProperty]
    public Movie Movie { get; set; }
    
  • 如果模型状态中存在错误(例如,ReleaseDate 无法被转换为日期),则会使用已提交的值重新显示表单。

  • 如果没有模型错误,则电影已保存。

“索引”、“创建”和“删除”Razor 页面中的 HTTP GET 方法遵循一个类似的模式。 “创建”Razor 页面中的 HTTP POST OnPostAsync 方法遵循的模式类似于“编辑”Razor 页面中的 OnPostAsync 方法所遵循的模式。

后续步骤