第 5 部分:编辑窗体和模板化
MVC 音乐存储是一个教程应用程序,它介绍并逐步说明如何使用 ASP.NET MVC 和 Visual Studio 进行 Web 开发。
MVC 音乐商店是一个轻量级的示例商店实现,用于在线销售音乐专辑,并实现基本的网站管理、用户登录和购物车功能。
本教程系列详细介绍了生成 ASP.NET MVC 音乐商店示例应用程序所执行的所有步骤。 第 5 部分介绍编辑窗体和模板化。
在上一章中,我们从数据库中加载并显示数据。 在本章中,我们还将启用编辑数据。
创建 StoreManagerController
首先,我们将创建一个名为 StoreManagerController 的新控制器。 对于此控制器,我们将利用 ASP.NET MVC 3 工具更新中提供的基架功能。 设置“添加控制器”对话框的选项,如下所示。
单击“添加”按钮时,你将看到 ASP.NET MVC 3 基架机制为你执行了大量工作:
- 它使用本地实体框架变量创建新的 StoreManagerController
- 它将 StoreManager 文件夹添加到项目的 Views 文件夹
- 它将 Create.cshtml、Delete.cshtml、Details.cshtml、Edit.cshtml 和 Index.cshtml 视图添加到 Album 类中强类型化
新的 StoreManager 控制器类包括 CRUD (创建、读取、更新、删除) 控制器操作,这些操作知道如何使用 Album 模型类并使用实体框架上下文进行数据库访问。
修改基架视图
请务必记住,虽然此代码是为我们生成的,但它是标准 ASP.NET MVC 代码,就像我们在本教程中一直在编写一样。 它旨在节省你花费在编写样板控制器代码和手动创建强类型视图上的时间,但这并不是那种生成的代码,你可能已经看到前面有关于你不得更改代码的可怕警告。 这是你的代码,你需要更改它。
因此,让我们先快速编辑 StoreManager 索引视图, (/Views/StoreManager/Index.cshtml) 。 此视图将显示一个表,其中列出了我们商店中包含“编辑/详细信息/删除”链接的相册,并包含相册的公共属性。 我们将删除 AlbumArtUrl 字段,因为它在此显示中不太有用。 在 <视图代码的表> 部分中,删除 <AlbumArtUrl 引用周围的 th> 和 <td> 元素,如以下突出显示的行所示:
<table>
<tr>
<th>
Genre
</th>
<th>
Artist
</th>
<th>
Title
</th>
<th>
Price
</th>
<th>
AlbumArtUrl
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Genre.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Artist.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.AlbumArtUrl)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.AlbumId }) |
@Html.ActionLink("Details", "Details", new { id=item.AlbumId }) |
@Html.ActionLink("Delete", "Delete", new { id=item.AlbumId })
</td>
</tr>
}
</table>
修改后的视图代码将如下所示:
@model IEnumerable<MvcMusicStore.Models.Album>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create
New", "Create")
</p>
<table>
<tr>
<th>
Genre
</th>
<th>
Artist
</th>
<th>
Title
</th>
<th>
Price
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Genre.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Artist.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.AlbumId }) |
@Html.ActionLink("Details", "Details", new { id=item.AlbumId }) |
@Html.ActionLink("Delete", "Delete", new { id=item.AlbumId })
</td>
</tr>
}
</table>
初步了解应用商店管理器
现在,运行应用程序并浏览到 /StoreManager/。 这会显示我们刚刚修改的应用商店管理器索引,其中显示了应用商店中包含“编辑”、“详细信息”和“删除”链接的相册列表。
单击“编辑”链接将显示包含“专辑”字段的编辑窗体,包括“流派”和“艺术家”的下拉列表。
单击底部的“返回列表”链接,然后单击相册的详细信息链接。 这会显示单个相册的详细信息。
再次单击“返回列表”链接,然后单击“删除”链接。 这会显示一个确认对话框,其中显示了专辑详细信息,并询问我们是否确定要删除它。
单击底部的“删除”按钮将删除相册,并返回到“索引”页,其中显示已删除的相册。
我们尚未完成应用商店管理器,但我们有可供 CRUD 操作从其开始的工作控制器和查看代码。
查看应用商店管理器控制器代码
应用商店管理器控制器包含大量代码。 让我们从上到下了解这一点。 控制器包括 MVC 控制器的一些标准命名空间,以及对 Models 命名空间的引用。 控制器具有 MusicStoreEntities 的专用实例,每个控制器操作使用这些数据访问。
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcMusicStore.Models;
namespace MvcMusicStore.Controllers
{
public class StoreManagerController : Controller
{
private MusicStoreEntities db = new MusicStoreEntities();
应用商店管理器索引和详细信息操作
索引视图检索专辑列表,包括每张专辑引用的流派和艺术家信息,正如我们之前在处理 Store Browse 方法时所看到的一样。 索引视图遵循对链接对象的引用,以便它可以显示每张专辑的流派名称和艺术家名称,因此控制器高效,并在原始请求中查询此信息。
//
// GET: /StoreManager/
public ViewResult Index()
{
var albums = db.Albums.Include(a => a.Genre).Include(a => a.Artist);
return View(albums.ToList());
}
StoreManager 控制器的详细信息控制器操作的工作方式与之前编写的存储控制器详细信息操作完全相同 - 它使用 Find () 方法按 ID 查询相册,然后将其返回到视图。
//
// GET: /StoreManager/Details/5
public ViewResult Details(int id)
{
Album album = db.Albums.Find(id);
return View(album);
}
创建操作方法
Create 操作方法与到目前为止看到的方法略有不同,因为它们处理表单输入。 当用户首次访问 /StoreManager/Create/ 时,将显示一个空窗体。 此 HTML 页面将包含一个 <表单元素,该元素> 包含下拉列表和文本框输入元素,可在其中输入相册的详细信息。
用户填写“相册”窗体值后,可以按“保存”按钮将这些更改提交回应用程序,以便在数据库中保存。 当用户按下“保存”按钮时,<表单>将执行 HTTP-POST 返回到 /StoreManager/Create/ URL,并将表单>值作为 HTTP-POST 的一部分提交<。
ASP.NET MVC 允许我们轻松拆分这两个 URL 调用方案的逻辑,方法是在 StoreManagerController 类中实现两个单独的“创建”操作方法 - 一个用于处理初始 HTTP-GET 浏览到 /StoreManager/Create/ URL,另一个处理提交的更改的 HTTP-POST。
使用 ViewBag 将信息传递到视图
在本教程的前面部分,我们使用了 ViewBag,但尚未对其进行太多介绍。 ViewBag 允许我们在不使用强类型模型对象的情况下将信息传递到视图。 在这种情况下,我们的编辑 HTTP-GET 控制器操作需要将流派列表和艺术家列表传递到窗体以填充下拉列表,最简单的方法是将它们作为 ViewBag 项返回。
ViewBag 是一个动态对象,这意味着你可以键入 ViewBag.Foo 或 ViewBag.YourNameHere,而无需编写代码来定义这些属性。 在这种情况下,控制器代码使用 ViewBag.GenreId 和 ViewBag.ArtistId,以便随表单提交的下拉值将为 GenreId 和 ArtistId,它们是它们要设置的专辑属性。
这些下拉列表值使用 SelectList 对象返回到窗体,该对象仅用于此目的而构建。 这是使用如下所示的代码完成的:
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name");
从操作方法代码中可以看到,三个参数用于创建此对象:
- 下拉列表将显示的项目列表。 请注意,这不仅仅是一个字符串 - 我们将传递流派列表。
- 传递给 SelectList 的下一个参数是所选值。 SelectList 知道如何预先选择列表中的项。 当我们查看“编辑”窗体时,这会更容易理解,这非常相似。
- 最后一个参数是要显示的属性。 在这种情况下,这表示 Genre.Name 属性将向用户显示。
考虑到这一点,HTTP-GET 创建操作非常简单 - 将两个 SelectList 添加到 ViewBag,并且不会将模型对象传递到窗体 (,因为尚未创建) 。
//
// GET: /StoreManager/Create
public ActionResult Create()
{
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name");
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name");
return View();
}
用于在创建视图中显示下拉列表的 HTML 帮助程序
由于我们已讨论如何将下拉值传递到视图,因此让我们快速查看视图,了解这些值的显示方式。 在 /Views/StoreManager/Create.cshtml) (视图代码中,你将看到以下调用以显示“流派”下拉列表。
@Html.DropDownList("GenreId",
String.Empty)
这称为 HTML 帮助程序 - 执行常见视图任务的实用工具方法。 HTML 帮助程序在保持视图代码简明易读方面非常有用。 Html.DropDownList 帮助程序由 ASP.NET MVC 提供,但正如我们稍后将看到的,可以创建自己的帮助程序来查看代码,我们将在应用程序中重复使用。
只需告知 Html.DropDownList 调用两件事 - 在何处获取要显示的列表,以及 (应预先选择任何) 的值。 第一个参数 GenreId 指示 DropDownList 在模型或 ViewBag 中查找名为 GenreId 的值。 第二个参数用于指示要在下拉列表中最初选择的值。 由于此窗体是“创建”窗体,因此没有要预先选择的值,并且传递了 String.Empty。
处理已发布的表单值
如前所述,有两种操作方法与每种表单相关联。 第一个 处理 HTTP-GET 请求并显示窗体。 第二个处理 HTTP-POST 请求,其中包含提交的表单值。 请注意,控制器操作具有 [HttpPost] 属性,该属性告知 ASP.NET MVC 它只应响应 HTTP-POST 请求。
//
// POST: /StoreManager/Create
[HttpPost]
public ActionResult Create(Album album)
{
if (ModelState.IsValid)
{
db.Albums.Add(album);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
此操作有四项责任:
-
- 读取表单值
-
- 检查表单值是否通过任何验证规则
-
- 如果表单提交有效,请保存数据并显示更新的列表
-
- 如果表单提交无效,请重新显示包含验证错误的表单
使用模型绑定读取表单值
控制器操作正在处理表单提交,该表单提交包含“Title”、“Price”和“AlbumArtUrl”下拉列表) 和文本框值的“GenreId”和“ArtistId” (的值。 虽然可以直接访问表单值,但更好的方法是使用内置于 ASP.NET MVC 中的模型绑定功能。 当控制器操作将模型类型作为参数时,ASP.NET MVC 将尝试使用表单输入 (以及路由和查询字符串值) 填充该类型的对象。 它通过查找名称与模型对象的属性匹配的值来执行此操作,例如,在设置新 Album 对象的 GenreId 值时,它会查找名为 GenreId 的输入。 使用 ASP.NET MVC 中的标准方法创建视图时,表单将始终使用属性名称作为输入字段名称呈现,因此,字段名称只会匹配。
验证模型
通过对 ModelState.IsValid 的简单调用来验证模型。 我们尚未向 Album 类添加任何验证规则-我们将稍后执行此操作-因此,现在此检查没有太多工作要做。 重要的是,此 ModelStat.IsValid 检查将适应我们在模型上放置的验证规则,因此将来对验证规则的更改不需要对控制器操作代码进行任何更新。
保存提交的值
如果表单提交通过验证,则是时候将值保存到数据库。 使用 Entity Framework 时,只需将模型添加到 Albums 集合并调用 SaveChanges 即可。
db.Albums.Add(album);
db.SaveChanges();
实体框架生成相应的 SQL 命令来保留值。 保存数据后,我们会重定向回相册列表,以便查看更新。 这是通过返回 RedirectToAction 和我们想要显示的控制器操作的名称来完成的。 在本例中,这是 Index 方法。
显示无效的表单提交和验证错误
如果表单输入无效,下拉列表值将添加到 ViewBag (如 HTTP-GET 事例) ,绑定模型值将传递回视图进行显示。 使用 @Html.ValidationMessageFor HTML 帮助程序自动显示验证错误。
测试创建窗体
若要对此进行测试,请运行应用程序并浏览到 /StoreManager/Create/ - 这将显示由 StoreController Create HTTP-GET 方法返回的空白表单。
填写一些值,然后单击“创建”按钮提交表单。
处理编辑
编辑操作对 (HTTP-GET 和 HTTP-POST) 与刚刚查看的 Create 操作方法非常相似。 由于编辑方案涉及使用现有相册,编辑 HTTP-GET 方法基于通过路由传入的“id”参数加载专辑。 通过 AlbumId 检索相册的此代码与之前在“详细信息”控制器操作中查看的代码相同。 与 Create /HTTP-GET 方法一样,下拉值通过 ViewBag 返回。 这使我们可以将 Album 作为模型对象返回到视图 (该视图) 强类型化为 Album 类,同时传递其他数据 (例如通过 ViewBag) 流派列表。
//
// GET: /StoreManager/Edit/5
public ActionResult Edit(int id)
{
Album album = db.Albums.Find(id);
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
“编辑 HTTP-POST”操作与“创建 HTTP-POST”操作非常相似。 唯一的区别在于,而不是将新相册添加到数据库。相册集合,我们将找到使用 db 的专辑的当前实例。条目 (专辑) ,并将其状态设置为“已修改”。 这告诉 Entity Framework 我们正在修改现有相册,而不是创建新相册。
//
// POST: /StoreManager/Edit/5
[HttpPost]
public ActionResult Edit(Album album)
{
if (ModelState.IsValid)
{
db.Entry(album).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
我们可以通过运行应用程序并浏览到 /StoreManger/,然后单击相册的“编辑”链接来对此进行测试。
这将显示由编辑 HTTP-GET 方法显示的“编辑”窗体。 填写一些值并单击“保存”按钮。
这会发布窗体,保存值,并将我们返回到“专辑”列表,显示值已更新。
处理删除
删除遵循与“编辑”和“创建”相同的模式,使用一个控制器操作显示确认表单,使用另一个控制器操作来处理表单提交。
HTTP-GET 删除控制器操作与我们以前的应用商店管理器详细信息控制器操作完全相同。
//
// GET: /StoreManager/Delete/5
public ActionResult Delete(int id)
{
Album album = db.Albums.Find(id);
return View(album);
}
我们使用“删除视图”内容模板显示强类型化为“相册”类型的窗体。
Delete 模板显示模型的所有字段,但我们可以简化一些操作。 将 /Views/StoreManager/Delete.cshtml 中的视图代码更改为以下内容。
@model MvcMusicStore.Models.Album
@{
ViewBag.Title = "Delete";
}
<h2>Delete Confirmation</h2>
<p>Are you sure you want to delete the album titled
<strong>@Model.Title</strong>?
</p>
@using (Html.BeginForm()) {
<p>
<input type="submit" value="Delete" />
</p>
<p>
@Html.ActionLink("Back to
List", "Index")
</p>
}
这将显示简化的“删除”确认。
单击“删除”按钮会将表单发回到服务器,服务器将执行 DeleteConfirmed 操作。
//
// POST: /StoreManager/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Album album = db.Albums.Find(id);
db.Albums.Remove(album);
db.SaveChanges();
return RedirectToAction("Index");
}
HTTP-POST 删除控制器操作执行以下操作:
-
- 按 ID 加载相册
-
- 删除相册并保存更改
-
- 重定向到索引,显示相册已从列表中删除
若要对此进行测试,请运行应用程序并浏览到 /StoreManager。 从列表中选择一个相册,然后单击“删除”链接。
此时会显示“删除”确认屏幕。
单击“删除”按钮会删除该相册,并将我们返回到“应用商店管理器索引”页,其中显示该相册已被删除。
使用自定义 HTML 帮助程序截断文本
我们的应用商店管理器索引页面有一个潜在问题。 我们的专辑标题和艺术家名称属性都可以足够长,可以抛出表格格式。 我们将创建自定义 HTML 帮助程序,以便轻松截断视图中的这些属性和其他属性。
Razor 的 @helper 语法使创建自己的帮助程序函数变得非常简单,以便在视图中使用。 打开 /Views/StoreManager/Index.cshtml 视图,直接在行后面 @model 添加以下代码。
@helper Truncate(string
input, int length)
{
if (input.Length <= length) {
@input
} else {
@input.Substring(0, length)<text>...</text>
}
}
此帮助程序方法采用字符串和允许的最大长度。 如果提供的文本短于指定的长度,则帮助程序按原样输出。 如果时间较长,则截断文本并呈现“...”其余部分。
现在,可以使用 Truncate 帮助程序来确保“专辑标题”和“艺术家名称”属性都小于 25 个字符。 下面显示了使用新 Truncate 帮助程序的完整视图代码。
@model IEnumerable<MvcMusicStore.Models.Album>
@helper Truncate(string input, int length)
{
if (input.Length <= length) {
@input
} else {
@input.Substring(0, length)<text>...</text>
}
}
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create
New", "Create")
</p>
<table>
<tr>
<th>
Genre
</th>
<th>
Artist
</th>
<th>
Title
</th>
<th>
Price
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Genre.Name)
</td>
<td>
@Truncate(item.Artist.Name, 25)
</td>
<td>
@Truncate(item.Title, 25)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.AlbumId }) |
@Html.ActionLink("Details", "Details", new { id=item.AlbumId }) |
@Html.ActionLink("Delete", "Delete", new { id=item.AlbumId })
</td>
</tr>
}
</table>
现在,当我们浏览 /StoreManager/ URL 时,专辑和标题将保持在最大长度以下。
注意:这显示了在一个视图中创建和使用帮助程序这一简单案例。 若要详细了解如何创建可在整个网站中使用的帮助程序,请参阅我的博客文章: http://bit.ly/mvc3-helper-options