ASP.NET Core应用程序14:创建表单应用程序

  前几章集中讨论了处理 HTML 表单一个方面的单个特性,有时很难看到它们如何组合在一起执行常见的任务。本章将介绍创建控制器、视图和 Razor Pages 的过程,这些页面支持具有创建,读取、更新和删除(CRUD)功能的应用程序。本章不介绍新的功能,目标是演示如何将标签助手模型绑定和模型验证等功能与 Entity Framework Core 结合使用。

1 准备工作

  本章继续使用上章项目。
  为准备本章,请把 Controllers 文件夹中 HomeController.cs 文件的内容替换为如下代码。

[AutoValidateAntiforgeryToken]
public class HomeController : Controller
{
    private DataContext context;
    private IEnumerable<Category> Categories => context.Categories;
    private IEnumerable<Supplier> Suppliers => context.Suppliers;
    public HomeController(DataContext data)
    {
        context = data;
    }
    public IActionResult Index()
    {
        return View(context.Products.
            Include(p => p.Category).Include(p => p.Supplier));
    }
}

  添加 Views/Home 文件夹中 Index.cshtm。

@model IEnumerable<Product>
@{
    Layout = "_SimpleLayout";
}

<h4 class="bg-primary text-white text-center p-2">Products</h4>
<table class="table table-sm table-bordered table-striped">
    <thead>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Price</th>
            <th>Category</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (Product p in Model)
        {
            <tr>
                <td>@p.ProductId</td>
                <td>@p.Name</td>
                <td>@p.Price</td>
                <td>@p.Category.Name</td>
                <td class="text-center">
                    <a asp-action="Details" asp-route-id="@p.ProductId"
                       class="btn btn-sm btn-info">Details</a>
                    <a asp-action="Edit" asp-route-id="@p.ProductId"
                       class="btn btn-sm btn-warning">Edit</a>
                    <a asp-action="Delete" asp-route-id="@p.ProductId"
                       class="btn btn-sm btn-danger">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
<a asp-action="Create" class="btn btn-primary">Create</a>

  接下来,更新 Product 类,以更改验证约束,删除模型级检査,并禁用用远程验证。

//[PhraseAndPrice(Phrase = "Small", Price = "100")]
public class Product
{
    public long ProductId { get; set; }
    [Required]
    [Display(Name = "Name")]
    public string Name { get; set; }
    [Column(TypeName = "decimal(8, 2)")]
    [Required(ErrorMessage = "Please enter a price")]
    [Range(1, 999999, ErrorMessage = "Please enter a positive price")]
    public decimal Price { get; set; }
    [PrimaryKey(ContextType = typeof(DataContext),
        DataType = typeof(Category))]
    //[Remote("CategoryKey", "Validation", 
    //     ErrorMessage = "Enter an existing key")]
    public long CategoryId { get; set; }
    public Category Category { get; set; }
    [PrimaryKey(ContextType = typeof(DataContext),
        DataType = typeof(Category))]
    //[Remote("SupplierKey", "Validation", 
    //    ErrorMessage = "Enter an existing key")]
    public long SupplierId { get; set; }
    public Supplier Supplier { get; set; }
}

  最后,在 Startup 类中禁用全局过滤器。

//services.Configure<MvcOptions>(opts =>
//{
//    opts.Filters.Add<HttpsOnlyAttribute>();
//    opts.Filters.Add(new MessageAttribute("This is the globally-scoped filter"));
//});

  使用浏览器请求 http://ocalhost:5000/controllers,这将显示一个产品列表,有-些锚定元素样式化为按钮,但直到添加了创建、编辑和删除对象的功能之后,这些元素才会有效。

2 创建 MVC 表单应用程序

  在接下来的部分中,将展示如何使用 MVC 控制器和视图执行核心数据操作。本章的后面使用 Razor 页面(Razor Pages)创建相同的功能。

2.1 准备视图模型和视图

  下面定义一个用于多个操作的表单,通过它的视图模型类进行配置。要创建视图模型类,向Models 文件夹添加一个名为 ProductViewModel.cs 的类文件。

public class ProductViewModel
{
    public Product Product { get; set; }
    public string Action { get; set; } = "Create";
    public bool ReadOnly { get; set; } = false;
    public string Theme { get; set; } = "primary";
    public bool ShowAction { get; set; } = true;
    public IEnumerable<Category> Categories { get; set; }
        = Enumerable.Empty<Category>();
    public IEnumerable<Supplier> Suppliers { get; set; }
        = Enumerable.Empty<Supplier>();
}

  这个类将允许控制器向其视图传递数据和显示设置。Product 属性提供要显示的数据。Categories 和 Suppliers 属性提供在需要时对 Category 和 Supplier 对象的访问。
  其他属性配置给用户呈现内容的方式:Action 属性给当前任务指定操作方法的名称,ReadOnly 属性指定用户是否可以编辑数据,Theme 属性指定内容的引导主题,ShowAction 属性用于控制提交表单的按钮可
见性。
  要创建允许用户与应用程序数据交互的视图,给 Views/Home 文件夹添加 ProductEditor.cshtmnl 的 Razor 视图。

@model ProductViewModel
@{
    Layout = "_SimpleLayout";
}

<partial name="_Validation" />

<h5 class="bg-@Model.Theme text-white text-center p-2">@Model.Action</h5>

<form asp-action="@Model.Action" method="post">
    <div class="form-group">
        <label asp-for="Product.ProductId"></label>
        <input class="form-control" asp-for="Product.ProductId" readonly />
    </div>
    <div class="form-group">
        <label asp-for="Product.Name"></label>
        <div>
            <span asp-validation-for="Product.Name" class="text-danger"></span>
        </div>
        <input class="form-control" asp-for="Product.Name"
               readonly="@Model.ReadOnly" />
    </div>
    <div class="form-group">
        <label asp-for="Product.Price"></label>
        <div>
            <span asp-validation-for="Product.Price" class="text-danger"></span>
        </div>
        <input class="form-control" asp-for="Product.Price"
               readonly="@Model.ReadOnly" />
    </div>
    <div class="form-group">
        <label asp-for="Product.CategoryId">Category</label>
        <div>
            <span asp-validation-for="Product.CategoryId" class="text-danger"></span>
        </div>
        <select asp-for="Product.CategoryId" class="form-control"
                disabled="@Model.ReadOnly"
                asp-items="@(new SelectList(Model.Categories,
                    "CategoryId", "Name"))">
            <option value="" disabled selected>Choose a Category</option>
        </select>
    </div>
    <div class="form-group">
        <label asp-for="Product.SupplierId">Supplier</label>
        <div>
            <span asp-validation-for="Product.SupplierId" class="text-danger"></span>
        </div>
        <select asp-for="Product.SupplierId" class="form-control"
                disabled="@Model.ReadOnly"
                asp-items="@(new SelectList(Model.Suppliers,
                    "SupplierId", "Name"))">
            <option value="" disabled selected>Choose a Supplier</option>
        </select>
    </div>
    @if (Model.ShowAction)
    {
        <button class="btn btn-@Model.Theme" type="submit">@Model.Action</button>
    }
    <a class="btn btn-secondary" asp-action="Index">Back</a>
</form>

  这个视图可能看起来很复杂,但它只结合了前面章节介绍的特性,一旦看到它的实际应用,它将变得更加清晰。这个视图的模型是一个 ProductViewModel 对象,它既提供了显示给用户的数据,也提供了有关数据应该如何显示的一些信息。
  对于 Product 类定义的每个属性,视图包含一组元素:描述属性的 label 元素、允许对值进行编辑的 imput 或 select 元素,以及显示验证消息的 span 元素。每个元素都配置了 asp-for 特性,确保标签助手为每个属性转换元素。这里有定义视图结构的 div 元素,所有元素都是用于样式化表单的引导 CSS 类的成员。

2.2 读取数据

  最简单的操作是从数据库中读取数据并将其呈现给用户。在大多数应用程序中,这将允许用户看到列表视图中没有的额外细节。应用程序执行的每个任务都需要一组不同的 ProductViewModel 属性。为了管理这些组合,给 Models 文件夹添加一个名为 ViewModelFactory.cs 的类文件。

public static class ViewModelFactory
{
    public static ProductViewModel Details(Product p)
    {
        return new ProductViewModel
        {
            Product = p,
            Action = "Details",
            ReadOnly = true,
            Theme = "info",
            ShowAction = false,
            Categories = p == null ? Enumerable.Empty<Category>()
                : new List<Category> { p.Category },
            Suppliers = p == null ? Enumerable.Empty<Supplier>()
                : new List<Supplier> { p.Supplier },
        };
    }
}

  Details 方法生成一个为査看对象而配置的 ProducfViewModel 对象。当用户査看详细信息 Category 和 Supplier 信息时是只读的,这意味着只需要提供当前的 Category 和 Supplier 信息。
  接下来,向使用 VewModelFactoryDetais 方法的主控制器添加一个操作方法,创建 ProductViewModel对象,并使用 ProductEditor 视图将其显示给用户。

public async Task<IActionResult> Details(long id)
{
    Product p = await context.Products
        .Include(p => p.Category)
        .Include(p => p.Supplier)
        .FirstOrDefaultAsync(p => p.ProductId == id);
    ProductViewModel model = ViewModelFactory.Details(p);
    return View("ProductEditor", model);
}

  操作方法使用 id 参数(从路由数据中绑定模型)来查询数据库,并将 product 对象传递给ViewModelFactory.Details 方法。大多数操作都需要 Catcgory 和 Supplier 数据,因此添加了提供对数据的直接访问的属性。
  要测试 Details 特性,请求 http:/localhost:5000/controllers。单击其中一个Deaiis 按钮,将看到使用 ProductEditor 视图以只读形式显示选择的对象。

2.3 创建数据

  创建数据依赖于模型绑定从请求中获取表单数据,并依赖验证来确保数据可以存储在数据库中。第一步是添加一个工厂方法,创建用于创建数据的视图模型对象。在 Models 文件夹的 ViewModelFactory.cs 文件中添加创建方法。

public static ProductViewModel Create(Product product,
    IEnumerable<Category> categories, IEnumerable<Supplier> suppliers)
{
    return new ProductViewModel
    {
        Product = product,
        Categories = categories,
        Suppliers = suppliers
    };
}

  为 ProductViewModel 属性所使用的默认值是为创建数据而设置的,因此 Create 方法只设置 Product、Categories 和 Suppliers 属性。在 Home 控制器中创建数据操作方法。

public IActionResult Create()
{
    return View("ProductEditor",
        ViewModelFactory.Create(new Product(), Categories, Suppliers));
}
[HttpPost]
public async Task<IActionResult> Create([FromForm] Product product)
{
    if (ModelState.IsValid)
    {
        product.ProductId = default;
        product.Category = default;
        product.Supplier = default;
        context.Products.Add(product);
        await context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View("ProductEditor",
        ViewModelFactory.Create(product, Categories, Suppliers));
}

  有两个 Create 方法,它们通过 HttpPost 属性和方法参数进行区分。HTTP GET 请求将由第一个方法处理,该方法选择 ProductEditor 视图并为其提供一个 ProductViewModel 对象。当用户提交表单时,它将被第二种方法接收,该方法依赖模型绑定来接收数据,并通过模型验证来确保数据有效。
  如果数据通过验证,那么通过重置三个属性来准备存储在数据库中的对象。Entity Framework Core 配置数据库,以便在存储新数据时由数据库服务器分配主键。如果试图存储一个对象并提供一个不为零的 Productld 值,将抛出一个异常。
  重置了 Category 和 Supplier 属性,以防止存储对象时,EF Core 试图处理相关数据。注意,当验证失败时,使用参数调用 View 方法。
  之所以这样做,是因为视图期望的视图模型对象与前面使用模型绑定从请求中提取的数据类型不同。相反,这里创建了一个新的视图模型对象,该对象包含模型绑定数据,并将其传递给 View 方法。
  重启 ASP.NET Core,请求 http://hocalhost:5000/controllers,然后单击 Create。填写表单并单击 Create 按钮提交数据。新对象存储在数据库中,并在浏览器重定向到 Index 操作时显示出来。

2.4 编辑数据

  向视图模型工厂添加一个新方法,该方法将配置向用户显示数据的方式,Models 文件夹的 ViewModelFactory.cs 文件中添加编辑方法。
向在 Controllers 文件夹的 HomeController.cs 添加操作方法,向用户显示 Product 对象的当前属性,并接收用户所做的更改。

public async Task<IActionResult> Edit(long id)
{
    Product p = await context.Products.FindAsync(id);
    ProductViewModel model = ViewModelFactory.Edit(p, Categories, Suppliers);
    return View("ProductEditor", model);
}
[HttpPost]
public async Task<IActionResult> Edit([FromForm] Product product)
{
    if (ModelState.IsValid)
    {
        product.Category = default;
        product.Supplier = default;
        context.Products.Update(product);
        await context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View("ProductEditor",
        ViewModelFactory.Edit(product, Categories, Suppliers));
}

2.5 删除数据

  在 Models 文件夹的 ViewModelFactory.cs 文件中添加删除方法。

public static ProductViewModel Delete(Product p,
    IEnumerable<Category> categories, IEnumerable<Supplier> suppliers)
{
    return new ProductViewModel
    {
        Product = p,
        Action = "Delete",
        ReadOnly = true,
        Theme = "danger",
        Categories = categories,
        Suppliers = suppliers
    };
}

  将删除操作方法添加到 Home 控制器中。

public async Task<IActionResult> Delete(long id)
{
    ProductViewModel model = ViewModelFactory.Delete(
        await context.Products.FindAsync(id), Categories, Suppliers);
    return View("ProductEditor", model);
}
[HttpPost]
public async Task<IActionResult> Delete(Product product)
{
    context.Products.Remove(product);
    await context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}

3 创建 Razor Pages 表单应用程序

   Pages 文件夹中创建 Index.cshtml Razor Pages,并使用浏览器请求 http://localhost:5000/pages

@page "/pages/{id:long?}"
@model IndexModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@using Microsoft.EntityFrameworkCore

<div class="m-2">
    <h4 class="bg-primary text-white text-center p-2">Products</h4>
    <table class="table table-sm table-bordered table-striped">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Price</th>
                <th>Category</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (Product p in Model.Products)
            {
                <tr>
                    <td>@p.ProductId</td>
                    <td>@p.Name</td>
                    <td>@p.Price</td>
                    <td>@p.Category.Name</td>
                    <td class="text-center">
                        <a asp-page="Details" asp-route-id="@p.ProductId"
                           class="btn btn-sm btn-info">Details</a>
                        <a asp-page="Edit" asp-route-id="@p.ProductId"
                           class="btn btn-sm btn-warning">Edit</a>
                        <a asp-page="Delete" asp-route-id="@p.ProductId"
                           class="btn btn-sm btn-danger">Delete</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <a asp-page="Create" class="btn btn-primary">Create</a>
</div>

@functions
{

    public class IndexModel : PageModel
    {
        private DataContext context;

        public IndexModel(DataContext dbContext)
        {
            context = dbContext;
        }

        public IEnumerable<Product> Products { get; set; }

        public void OnGetAsync(long id = 1)
        {
            Products = context.Products
                .Include(p => p.Category).Include(p => p.Supplier);
        }
    }
}

3.1 创建常用功能

  这里不在示例应用程序所需的每个页面中复制相同的 HTML 表单和支持代码。相反,定义一个部分视图来定义 HTM 表单,再定义一个基类,来定义页面模型类所需的公共代码。对于部分视图,给 pages 文件夹添加一个名为 _ProductEditor.cshtml。

@model ProductViewModel

<partial name="_Validation" />

<h5 class="bg-@Model.Theme text-white text-center p-2">@Model.Action</h5>

<form asp-page="@Model.Action" method="post">
    <div class="form-group">
        <label asp-for="Product.ProductId"></label>
        <input class="form-control" asp-for="Product.ProductId" readonly />
    </div>
    <div class="form-group">
        <label asp-for="Product.Name"></label>
        <div>
            <span asp-validation-for="Product.Name" class="text-danger"></span>
        </div>
        <input class="form-control" asp-for="Product.Name"
               readonly="@Model.ReadOnly" />
    </div>
    <div class="form-group">
        <label asp-for="Product.Price"></label>
        <div>
            <span asp-validation-for="Product.Price" class="text-danger"></span>
        </div>
        <input class="form-control" asp-for="Product.Price"
               readonly="@Model.ReadOnly" />
    </div>
    <div class="form-group">
        <label asp-for="Product.CategoryId">Category</label>
        <div>
            <span asp-validation-for="Product.CategoryId" class="text-danger"></span>
        </div>
        <select asp-for="Product.CategoryId" class="form-control"
                disabled="@Model.ReadOnly" 
                asp-items="@(new SelectList(Model.Categories,"CategoryId", "Name"))">
            @*<option value="-1">Create New Category...</option>*@
            <option value="" disabled selected>Choose a Category</option>
        </select>
    </div>
    @*<partial name="_CategoryEditor" for="Product" />*@
    <div class="form-group">
        <label asp-for="Product.SupplierId">
            Supplier
        </label>

        <div>
            <span asp-validation-for="Product.SupplierId" class="text-danger"></span>
        </div>
        <select asp-for="Product.SupplierId" class="form-control" 
                disabled="@Model.ReadOnly"
                asp-items="@(new SelectList(Model.Suppliers,
                "SupplierId", "Name"))">
            <option value="" disabled selected>Choose a Supplier</option>
        </select>
    </div>

    @if (Model.ShowAction)
    {
        <button class="btn btn-@Model.Theme" type="submit">@Model.Action</button>
    }
    <a class="btn btn-secondary" asp-page="Index">Back</a>
</form>

  部分视图使用 ProductViewModel 类作为它的模型类型,并依赖内置的标签助手为 Product 类定义的属性显示 input 和 select 元素。这与本章前面使用的内容相同,只是将 asp-action 属性替换为 asp-page,以指定 form 和 anchor 元素的目标。要定义页面模型基类,向 Pages 文件夹添加一个名为 EditorPageModel.cs 的类文件。

public class EditorPageModel : PageModel
{
    public EditorPageModel(DataContext dbContext)
    {
        DataContext = dbContext;
    }
    public DataContext DataContext { get; set; }
    public IEnumerable<Category> Categories => DataContext.Categories;
    public IEnumerable<Supplier> Suppliers => DataContext.Suppliers;
    public ProductViewModel ViewModel { get; set; }
}

  向 Pages 文件夹下的 _ViewImports.cshtml 添加名称空间。

@namespace MyWebApp.Pages
@using MyWebApp.Models
@addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *,MyWebApp
@using Microsoft.AspNetCore.Mvc.RazorPages
@using Microsoft.EntityFrameworkCore
@using MyWebApp.Pages
@using System.Text.Json
@using Microsoft.AspNetCore.Http

3.2 为 CRUD 操作定义页面

  有了部分视图和共享基类,处理各个操作的页面就十分简单了。给 Pages 文件夹添加一个名Details.cshtml 的Razor Pages。

@page "/pages/details/{id}"
@model DetailsModel

<div class="m-2">
    <partial name="_ProductEditor" model="@Model.ViewModel" />
</div>

@functions
{
    public class DetailsModel : EditorPageModel
    {
        public DetailsModel(DataContext dbContext) : base(dbContext) { }
        public async Task OnGetAsync(long id)
        {
            Product p = await DataContext.Products.
                Include(p => p.Category).Include(p => p.Supplier)
                .FirstOrDefaultAsync(p => p.ProductId == id);
            ViewModel = ViewModelFactory.Details(p);
        }
    }
}

  给Pages 文件夹添加一个名为 Create.cshtml 的 Razor Pages。

@page "/pages/create"
@model CreateModel

<div class="m-2">
    <partial name="_ProductEditor" model="@Model.ViewModel" />
</div>

@functions 
{
    public class CreateModel : EditorPageModel
    {
        public CreateModel(DataContext dbContext) : base(dbContext) { }

        public void OnGet()
        {
            ViewModel = ViewModelFactory.Create(p, Categories, Suppliers);
        }
        public async Task<IActionResult> OnPostAsync([FromForm] Product product)
        {
            if (ModelState.IsValid)
            {
                product.ProductId = default;
                product.Category = default;
                product.Supplier = default;
                DataContext.Products.Add(product);
                await DataContext.SaveChangesAsync();
                return RedirectToPage(nameof(Index));
            }
            ViewModel = ViewModelFactory.Create(product, Categories, Suppliers);
            return Page();
        }
    }
}

  给 Pages 文件夹添加一个名为 Edit.cshtml 的 RazorPages。

@page "/pages/edit/{id}"
@model EditModel

<div class="m-2">
    <partial name="_ProductEditor" model="@Model.ViewModel" />
</div>

@functions 
{
    public class EditModel : EditorPageModel
    {
        public EditModel(DataContext dbContext) : base(dbContext) { }

        public async Task OnGetAsync(long id)
        {
            Product p = await this.DataContext.Products.FindAsync(id);
            ViewModel = ViewModelFactory.Edit(p, Categories, Suppliers);
        }
        public async Task<IActionResult> OnPostAsync([FromForm] Product product)
        {
            if (ModelState.IsValid)
            {
                product.Category = default;
                product.Supplier = default;
                DataContext.Products.Update(product);
                await DataContext.SaveChangesAsync();
                return RedirectToPage(nameof(Index));
            }
            ViewModel = ViewModelFactory.Edit(product, Categories, Suppliers);
            return Page();
        }
    }
}

  给 Pages 文件夹添加一个名为 Delete.cshtml的 Razor Pages。

@page "/pages/delete/{id}"
@model DeleteModel

<div class="m-2">
    <partial name="_ProductEditor" model="@Model.ViewModel" />
</div>

@functions 
{
    public class DeleteModel : EditorPageModel
    {
        public DeleteModel(DataContext dbContext) : base(dbContext) { }

        public async Task OnGetAsync(long id)
        {
            ViewModel = ViewModelFactory.Delete(
                await DataContext.Products.FindAsync(id), Categories, Suppliers);
        }
        public async Task<IActionResult> OnPostAsync([FromForm] Product product)
        {
            DataContext.Products.Remove(product);
            await DataContext.SaveChangesAsync();
            return RedirectToPage(nameof(Index));
        }
    }
}

  重启 ASP.NET Core 并导航到 http://localhost:5000/pages,将能够单击链接,来查看、创建编辑和删除数据。

4 创建新的相关数据对象

  一些应用程序需要允许用户创建新的相关数据,这样,例如,新的类别可以与该类别中的产品一起创建。有两种方法可以解决这个问题。

4.1 在同一请求中提供相关数据

  第一种方法是要求用户提供以相同形式创建相关数据所需的数据。以用户输入Product 对象值的相同形式来收集 Category 对象信息,这对于简单数据类型是种有用的方法。
  给 Pages 文件夹添加为 _CategoryEditor.cshtml 的 Razor 视图,在各自的部分视图中定义相关数据类型的 HTML 元素。

@model Product
<script type="text/javascript">
    $(document).ready(() => {
        const catGroup = $("#categoryGroup").hide();
        $("select[name='Product.CategoryId']").on("change", (event) =>
            event.target.value === "-1" ? catGroup.show() : catGroup.hide());
    });
</script>

<div class="form-group bg-info p-1" id="categoryGroup">
    <label class="text-white" asp-for="Category.Name">
        New Category Name
    </label>
    <input class="form-control" asp-for="Category.Name" value="" />
</div>

  Category 类型只需要一个属性,用户使用标准 input 元素提供该属性。部分视图中的 script 元素包含隐藏新元素的 jQuery 代码,直到用户选择为 Product.CategoryId 属性设置 -1 值的 option 元素为止。
  在 Pages 文件夹的 _ProductEditor.cshtml 向 编辑 添加了部分视图,还显示 option 元素,该元素显示用于创建新 Category 对象的元素。

<option value="-1">Create New Category...</option>
<partial name="_CategoryEditor" for="Product" />

  需要在多个页面中使用新功能,因此为了避免代码重复,在 Pages 文件夹的 EditorPageModel.cs 文件中添加一个方法来处理页面模型基类中的相关数据。

protected async Task CheckNewCategory(Product product)
{
    if (product.CategoryId == -1 && 
        !string.IsNullOrEmpty(product.Category?.Name))
    {
        DataContext.Categories.Add(product.Category);
        await DataContext.SaveChangesAsync();
        product.CategoryId = product.Category.CategoryId;
        ModelState.Clear();
        TryValidateModel(product);
    }
}

  新代码使用从用户接收到的数据创建一个 Category 对象,并将其存储在数据库中。数据库服务器为新对象分配一个主键,EF Core 使用该主键来更新 Category 对象。这允许更新 Product 对象的 CategopyId 属性,重新验证模型数据;分配给 CategopyId 属性的值将通过验证,应为它对应于新分配的键。要将新功能集成到 Create 页面中,在 Pages 文件夹的 Create.cshtm 文件中添加语句。向 Edit 页面中的处理程序方法添加相同的语句。

public async Task<IActionResult> OnPostAsync([FromForm] Product product)
{
    await CheckNewCategory(product);
    ......
}

  重启并使用浏览器请求 http://localhost:5000/pages/edit/1。单击 Category 元素并从选项列表中选择 Create New Category。在 input 元素中输入新的类别名称,然后单击 Edit 按钮。当处理请求时,将一个新的 Categony 对象存储在数据库中并与 Product 对象相关联。

4.2 创建新数据

  对于具有自己复杂创建过程的相关数据类型,向主表单添加元素可能让用户不知所措;更好的方法是从主表单导航到另一个控制器或页面,让用户创建新对象,然后返回来完成原始任务。下面演示用于创建 Supplier 对象的技术,尽管 Supplier 类型很简单,只需要用户提供两个值。
  要创建允许用户创建 Supplier 对象的表单,给 Pages 文件夹添加名为 SupplierBreakOut.cshtml 的 Razor Pages。

@page "/pages/supplier"
@model SupplierPageModel

<div class="m-2">
    <h5 class="bg-secondary text-white text-center p-2">New Supplier</h5>
    <form asp-page="SupplierBreakOut" method="post">
        <div class="form-group">
            <label asp-for="Supplier.Name"></label>
            <input class="form-control" asp-for="Supplier.Name" />
        </div>
        <div class="form-group">
            <label asp-for="Supplier.City"></label>
            <input class="form-control" asp-for="Supplier.City" />
        </div>
        <button class="btn btn-secondary" type="submit">Create</button>
        <a class="btn btn-outline-secondary"
           asp-page="@Model.ReturnPage" asp-route-id="@Model.ProductId">
            Cancel
        </a>
    </form>
</div>

@functions 
{
    public class SupplierPageModel : PageModel
    {
        private DataContext context;

        public SupplierPageModel(DataContext dbContext)
        {
            context = dbContext;
        }

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

        public string ReturnPage { get; set; }
        public string ProductId { get; set; }

        public void OnGet([FromQuery(Name = "Product")] Product product,
                string returnPage)
        {
            TempData["product"] = Serialize(product);
            TempData["returnAction"] = ReturnPage = returnPage;
            TempData["productId"] = ProductId = product.ProductId.ToString();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            context.Suppliers.Add(Supplier);
            await context.SaveChangesAsync();
            Product product = Deserialize(TempData["product"] as string);
            product.SupplierId = Supplier.SupplierId;
            TempData["product"] = Serialize(product);
            string id = TempData["productId"] as string;
            return RedirectToPage(TempData["returnAction"] as string,
                new { id = id });
        }

        private string Serialize(Product p) => JsonSerializer.Serialize(p);
        private Product Deserialize(string json) =>
            JsonSerializer.Deserialize<Product>(json);
    }
}

  用户使用 GET 请求导航到此页面,该请求包含用户提供的产品的详细信息和用户应该返回到的页面的名称。使用 TempData 功能来存储该数据。
  此页面向用户显示一个表单,其中包含创建新 Supplier 对象所需的 Name 和 City 属性字段。提交表单时,POST 处理程序方法存储一个新的 Supplier 对象,并使用数据库服务器分配的键来更新 product 对象,然后将其再次存储为临时数据。用户重定向到到达的页面。
  向 Pages 文件夹的 _ProductEditor.cshtml 部分视图添加了元素,允许用户导航到新页面。

<label asp-for="Product.SupplierId">
    Supplier
    @if (!Model.ReadOnly) { 
        <input type="hidden" name="returnPage" value="@Model.Action" />
        <button class="btn btn-sm btn-outline-primary ml-3" 
                asp-page="SupplierBreakOut" formmethod="get" formnovalidate>
            Create New Supplier
        </button>
    }
</label>

  新元素添加了一个隐藏的 input 元素(该元素捕获要返回的页面),以及一个 button 元素(该元素使用 GET请求将表单数据提交到 SupplierBreakOut页面,这意味着表单值编码到查询字符串中)。
  在Pages 文件夹的 Create.cshtml 文件中所需的更改,以添加对检索临时数据和使用它填充 Product表单的支持。

public void OnGet() {
    Product p = TempData.ContainsKey("product") 
        ? JsonSerializer.Deserialize<Product>(TempData["product"] as string) 
        : new Product();
    ViewModel = ViewModelFactory.Create(p, Categories, Suppliers);
}

  在 Edit 页面中需要进行类似的更改。

public async Task OnGetAsync(long id) {
    Product p = TempData.ContainsKey("product") 
        ? JsonSerializer.Deserialize<Product>(TempData["product"] as string) 
        : await this.DataContext.Products.FindAsync(id);
    ViewModel = ViewModelFactory.Edit(p, Categories, Suppliers);
}

  其效果是向用户显示 Create New Supplier 按钮,该按钮向浏览器发送到一个表单,该表单可用于创建 Supplier 对象。一旦 Supplier 存储在数据库中,浏览器就发送回原始页面,表单中填充用户输入的数据,并且 Supplier select 元素设置为新创建的对象。

posted @ 2024-06-21 14:34  一纸年华  阅读(8)  评论(0编辑  收藏  举报