之前的示例中,VS 为我们自动生成了视图,这个特性很有用,但最终得到的视图太过简单并且需要根据数据模型类型进行裁剪。
例如,添加一个产品时,有一个用于用户输入的 ProductID 值和 Discontinued 值的字段。我们并不希望用户输入这些值,更何况 ProductID 值是表的主键且可以自动生成。我们也不希望用户在一个布尔类型的字段中任意输入值。
这一部分我们就来演示如何使用 MVC 视图更好的和数据模型约束协作,并使之更好的和整体应用程序相适应。为了掌握 MVC 视图,你必须知道 3 个组件,它们是模型数据、视图数据、HTML 辅助方法。
观察 Details.aspx 视图。MVC 视图的页面定义指定了它要使用的数据模型类型。下面是 Details 视图的页面定义,它用于显示 Products 类:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<BasicMvcApplication.Models.Products>" %>
要显示的类型成员可以通过对 Model 的引用获得,在 Details 视图中到处都可以见到它们(每个视图的 Model 都是被页面定义好的类型的实体):
<div class="display-label">ProductID</div>
<div class="display-field"><%: Model.ProductID %></div>
<div class="display-label">ProductName</div>
<div class="display-field"><%: Model.ProductName %></div>
<div class="display-label">SupplierID</div>
<div class="display-field"><%: Model.SupplierID %></div>
正是由于这些对 Model 的调用创建了 Details 视图。我们首先要做的事是整理显示。
我们期望在一个表格中显示产品的细节,而不是只罗列字段名称和值:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<BasicMvcApplication.Models.Products>" %>
<asp:Content ID="Content1" COntentPlaceHolderID="TitleContent" runat="server">
Details
</asp:Content>
<asp:Content ID="Content2" COntentPlaceHolderID="MainContent" runat="server">
<h2>Details</h2>
<fieldset>
<legend>Product Details</legend>
<table>
<tr><td>Product Name:</td><td><%: Model.ProductName %></td></tr>
<tr><td>Supplier ID:</td><td><%: Model.SupplierID %></td></tr>
<tr><td>Category ID:</td><td><%: Model.CategoryID %></td></tr>
<tr><td>Quantity per Unit:</td><td><%: Model.QuantityPerUnit %></td></tr>
<tr><td>Unit Price:</td><td><%: Model.UnitPrice %></td></tr>
<tr><td>Units in Stock:</td><td><%: Model.UnitsInStock %></td></tr>
<tr><td>Units on Order:</td><td><%: Model.UnitsOnOrder %></td></tr>
<tr><td>Recorder Level:</td><td><%: Model.ReorderLevel %></td></tr>
<tr><td>Discontinued:</td><td><%: Model.Discontinued %></td></tr>
</table>
</fieldset>
<p>
<%: Html.ActionLink("Edit", "Edit", new { id=Model.ProductID }) %> |
<%: Html.ActionLink("Back to List", "Index") %>
</p>
</asp:Content>
我们移除了对主键的引用,为了让数据模型正常工作我们需要它,但并不需要把它显示给用户。重构视图时,只是直接调用 Model.ProductID。
下一步是修改某些字段的显示方式。先从 UnitPrice 字段开始(在 MVC 视图里使用标准的 ASP.NET 特性):
<tr><td>Unit Price:</td><td><%: string.Format("{0:F2}", Model.UnitPrice)%></td></tr>
MVC 最棒的一个特性是 HTML 辅助方法,它简化了从模型数据生成 HTML 的过程。我们并不希望把布尔值显示为文本字符,现在做下面这个的修改:
<tr>
<td>Discontinued:</td>
<td><%: Html.CheckBoxFor(e => e.Discontinued, new {disabled="true"})%></td>
</tr>
这里,我们使用了 Html.CheckBoxFor 辅助方法。这个方法接受一个 Lambda 表达式以识别复选框关联的字段,并接受了一个可以用于指定额外 HTML 属性的对象。Lambda 表达式选中的 Discontinued 字段并设定 disabled 属性(因为我们在显示静态的细节)。
Details 视图被呈现时,对 HTML 辅助方法的调用生成定义复选框的 HTML 定义并设置状态来匹配模型数据。
下表是最常用的 Html 辅助方法:
HTML 方法 |
描 述 |
ActionLink | 创建调用此 MVC 应用程序控制器方法的链接 |
BeginForm | 创建将回发到控制器方法的表单 |
CheckBoxFor | 创建用于布尔值的复选框 |
DropDownListFor | 使用 SelectList 创建下拉列表 |
ListBoxFor | 创建允许多选的列表 |
PasswordFor | 创建适合输入密码的文本框 |
RadioButtonFor | 创建单选按钮 |
TextAreaFor | 创建多行文本输入区域 |
TextBoxFor | 创建单行的文本输入框 |
上表列出的辅助方法都是强类型的辅助方法,它们是在 MVC 2 中引入的。它们接受 Lambda 表达式,用于识别要为之生成的 HTML 的数据字段,如果字段不存在或不能够呈现为请求的 HTML 类型,它们会产生编译时错误。
辅助方法 ActionLink 用于生成回调 MVC 应用程序的链接,它也非常有用。默认的 Details 视图在页面底部有 2 个链接,这里再给它增加一个调用控制器中 Delete 方法的链接:
<%: Html.ActionLink("Delete", "Delete", new { id=Model.ProductID })%>|
现在查看详细页面,就可以在不返回 Index 视图下删除产品记录了:
Details 视图还是有一些问题。SupplierID 和 CategoryID 字段是访问其他表的关键,但这样的显示对用户毫无帮助。下面将演示如何通过视图数据特性来实现这一功能,它能够让控制器向视图传递模型数据之外的信息。
首先,要给 NorthwindAccessConsolidator 类添加方法,以便能够得到用户友好的类别和供应商名称:
public string GetSupplierName(Products prod)
{
return db.Suppliers
.Where(e => e.SupplierID == prod.SupplierID)
.Select(e => e.CompanyName)
.Single();
}
public string GetCategoryName(Products prod)
{
return db.Categories
.Where(e => e.CategoryID == prod.CategoryID)
.Select(e => e.CategoryName)
.Single();
}
然后更新控制器以获得类别和供应商、并把它们传递给视图,下面是更新后的 Details 方法:
public ActionResult Details(int id)
{
Products prod = nwa.GetProduct(id);
if (prod == null)
{
throw new NoSuchRecordException();
}
else
{
ViewData["CatName"] = nwa.GetCategoryName(prod);
ViewData["SupName"] = nwa.GetSupplierName(prod);
return View(prod);
}
}
这里要注意的是,通过 ViewData 集合可以向视图传送任意的数据,我们通过继承默认的控制器类获得了它。之后,视图就可以通过 ViewData 集合获取到传递的数据,下面是 Details.aspx 视图修改后的标记:
Supplier ID: <%: ViewData["SupName"] %>
Category ID: <%: ViewData["CatName"]%>
SupplierID 和 CategoryID 字段在数据模型中仍然可用,但我们选择不再使用它们。能够访问模型数据字段并轻松生成 HTML 的能力意味着我们能够方便的裁剪视图。仅通过一点点的努力我们就改进了 Details 视图,移除了不必要的元素、让部分元素的格式更加清晰并增加了额外的功能。
现在运行程序,Details 视图的效果应该像是这个样子:
现在,我们要来处理 Edit 视图。之前所说的绝大部分内容都可以应用到这个视图上,但仍然有些问题。我会演示其他的一些有用的 MVC 特性。下面是更新后的 Edit.aspx 视图:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<BasicMvcApplication.Models.Products>" %>
<asp:Content ID="Content1" COntentPlaceHolderID="TitleContent" runat="server">
Edit
</asp:Content>
<asp:Content ID="Content2" COntentPlaceHolderID="MainContent" runat="server">
<h2>Edit</h2>
<% using (Html.BeginForm()) {%>
<fieldset>
<legend>Edit Product Details</legend>
<table>
<tr><td>Product Name:</td><td><%: Html.TextBoxFor(e => e.ProductName) %></td></tr>
<tr><td>Supplier:</td><td><%: Html.TextBoxFor(e => e.SupplierID) %></td></tr>
<tr><td>Category:</td><td><%: Html.TextBoxFor(e => e.CategoryID) %></td></tr>
<tr><td>Quantity per Unit:</td><td><%: Html.TextBoxFor(e => e.QuantityPerUnit) %></td></tr>
<tr>
<td>Unit Price:</td>
<td><%: Html.TextBoxFor(e => e.UnitPrice,
new { Value = string.Format("{0:F2}", Model.UnitPrice) })%></td>
</tr>
<tr><td>Units in Stock:</td><td><%: Html.TextBoxFor(e => e.UnitsInStock) %></td></tr>
<tr><td>Units on Order:</td><td><%: Html.TextBoxFor(e => e.UnitsOnOrder) %></td></tr>
<tr><td>Reorder Level:</td><td><%: Html.TextBoxFor(e => e.ReorderLevel) %></td></tr>
<tr><td>Discontinued:</td><td><%: Html.TextBoxFor(e => e.Discontinued) %></td></tr>
</table>
</fieldset>
<p>
<input type="submit" value="Save" />
</p>
<% } %>
<div>
<%: Html.ActionLink("Back to List", "Index") %>
</div>
</asp:Content>
HTML 辅助方法 BeginForm 生成回发到控制器 URL 所需的表单 HTML,此处是 /Product/Edit/<产品 ID>。绝大部分数据字段使用了 HTML 辅助方法 TextBoxFor,它创建一个包含了我们指定的数据模型字段值的文本输入框。这里通过重写输入框的 value 属性来提供格式化后的 UnitPrice 值。但要注意,使用 string.Format 设置文本框的值时,要确保指定的 HTML 属性是 Value(首字母大写),否则 MVC 辅助方法将忽略格式化字符串。
现在运行程序,效果如下:
Supplier 和 Category 还有问题,但先前的解决方案已经不起作用。因为需要从列表进行选择,而不是仅仅看到单一的值。下面我们将通过扩展数据模型并使用视图数据以及 HTML 辅助方法来解决这一问题。
首先要扩展 NorthwindAccessConsolidator 类以获取供应商及类别名称的完整列表,然后从名称字符串取得 SupplierID 和 CategoryID:
public IEnumerable<string> GetAllSuppliers()
{
return db.Suppliers.Select(e => e.CompanyName);
}
public int GetSupplierID(string name)
{
return db.Suppliers.Where(e => e.CompanyName == name)
.Select(e => e.SupplierID).Single();
}
public IEnumerable<string> GetAllCategories()
{
return db.Categories.Select(e => e.CategoryName);
}
public int GetCategoryID(string name)
{
return db.Categories.Where(e => e.CategoryName == name)
.Select(e => e.CategoryID).Single();
}
接着我们在 Models 目录中添加一个名为 ProductListWrapper 的类来扩展数据模型。它是一个简单的封装类:
namespace ExtendedModel.Models
{
public class ProductListWrapper
{
public Products product { get; set; }
public string SelectedSupplier { get; set; }
public string SelectedCategory { get; set; }
}
}
修改控制器中第一个 Edit 方法以使它通过 View 方法返回 ProductListWrapper 的实例。用户在 Index 视图中单击 Edit 链接时会调用此方法。我们在方法中通过视图数据将 SelectList 类的实例传给视图。它是一个特殊的 MVC 类型,可以用来传送要呈现的对象列表。
public ActionResult Edit(int id)
{
ViewData["categories"] = new SelectList(nwa.GetAllCategories());
ViewData["suppliers"] = new SelectList(nwa.GetAllSuppliers());
Products prod = nwa.GetProduct(id);
ProductListWrapper wrap = new ProductListWrapper()
{
product = prod,
SelectedCategory = prod.Categories.CategoryName,
SelectedSupplier = prod.Suppliers.CompanyName
};
return View(wrap);
}
接着,要更新 Edit.aspx,这样视图才知道如何呈现新数据类型。首先修改页面定义使之指向封装类型:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<ExtendedModel.Models.ProductListWrapper>" %>
视图中的模型引用要修改为 e.product.Fields 格式以反映封装类型的结构:
<tr><td>Product Name:</td><td><%: Html.TextBoxFor(e => e.product.ProductName) %></td></tr>
......
通过 HTML 辅助方法 DropDownListFor 来达到使用视图数据中新增的 SelectList 实例:
<tr>
<td>Supplier:</td>
<td><%: Html.DropDownListFor(e => e.SelectedSupplier,
ViewData["suppliers"] as SelectList)%></td>
</tr>
<tr>
<td>Category:</td>
<td><%: Html.DropDownListFor(e => e.SelectedCategory,
ViewData["categories"] as SelectList)%></td>
</tr>
最后一步是修改回发修改时调用的控制器方法 Edit。对于这个方法,我们需要解压被封装的 Product 实例,更新 SupplierID 和 CategoryID 的值让它们和用户的选择相匹配,然后保存:
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
try
{
Products prod = nwa.GetProduct(id);
if (prod != null)
{
ProductListWrapper wrapper = new ProductListWrapper()
{
product = prod
};
UpdateModel(wrapper);
prod.SupplierID = nwa.GetSupplierID(wrapper.SelectedSupplier);
prod.CategoryID = nwa.GetCategoryID(wrapper.SelectedCategory);
nwa.SaveChanges();
return RedirectToAction("Index");
}
else
{
throw new NoSuchRecordException();
}
}
catch
{
return View();
}
}
现在运行程序,呈现的效果如下:
通过向数据模型增加一些简单的代码,同时更新控制器和视图,就可以把数值型的外键映射为用户能够理解并可从列表选择的内容。通过很少的调整就可以创造很大的价值,这展示了 MVC 框架的灵活性。