ASP.NET MVC with Entity Framework and CSS一书翻译系列文章之第二章:利用模型类创建视图、控制器和数据库
在这一章中,我们将直接进入项目,并且为产品和分类添加一些基本的模型类。我们将在Entity Framework的代码优先模式下,利用这些模型类创建一个数据库。我们还将学习如何在代码中创建数据库上下文类、指定数据库连接字符串以及创建一个数据库。最后,我们还将添加视图和控制器来管理和显式产品和分类数据。
注意:如果你想按照本章的代码编写示例,你必须完成第一章或者直接从www.apress.com下载第一章的源代码。
2.1 添加模型类
Entity Framework的代码优先模式允许我们从模型类创建数据库。我们将创建表示产品和分类的两个模型类来开始本章的学习。我们还将在产品和分类之间添加0或1对多的关系,表示一个产品可以属于一个分类或不属于任何分类,一个分类可以包含多个产品。
右击Models文件夹,然后从菜单中选择【添加】->【类】,创建一个名为“Product”的新类,然后在该类中添加如下代码:
1 namespace BabyStore.Models 2 { 3 public class Product 4 { 5 public int ID { get; set; } 6 public string Name { get; set; } 7 public string Description { get; set; } 8 public decimal Price { get; set; } 9 public int? CategoryID { get; set; } 10 public virtual Category Category { get; set; } 11 } 12 }
在文件的上方移除所有不必要的using语句。
提示:要移除不不必要的using语句,我们只需将光标悬停在using语句上,点击出现的黄色灯泡,然后选择“删除不必要的using”选项即可。
Product类包含以下属性:
- ID—用于表示产品(product)在数据库中的主键。
- Name—产品(product)的名称。
- Description—关于产品的文本描述。
- Price—表示产品的价格。
- CategoryID—表示指定给产品(product)的分类(category)ID。在数据库中通常被设置为外键。我们还将该属性的类型设置为可空整型(int?),用于描述这样一个事实:某个产品可能不属于任何分类。这可以避免在对分类执行删除操作时,该分类下的所有产品也被删除的情况发生。默认情况下,Entity Framework对可空类型外键启用级联删除,也就是说,如果CategoryID不是可空的,当一个分类被删除时,所有与该分类相关联的产品也会被删除。
- Category—导航属性。导航属性包含与该实体相关的其他实体,在这种情况下,这个属性将包含该产品所属的分类实体。如果一个导航数据可以包含多个实体,那么它必须被定义为集合类型。通常使用ICollection类型。导航属性通常被定义为virtual,以便能够完成某些特殊的功能,比如延迟加载。
提示:我们可以在Visual Studio中输入prop,然后按两次Tab键来自动生成属性。
下面,我们在Models文件夹下再新建一个名为“Category”的模型类,然后在该类中添加如下代码:
1 using System.Collections.Generic; 2 3 namespace BabyStore.Models 4 { 5 public class Category 6 { 7 public int ID { get; set; } 8 public string Name { get; set; } 9 public virtual ICollection<Product> Products { get; set; } 10 } 11 }
移除文件上方所有不必要的using语句。Category类包含的属性如下所示:
- ID—主键。
- Name—分类的名称。
- Products—导航属性,该属性包含属于该分类的所有产品实体。
2.2 添加数据库上下文
对于数据模型来说,数据库上下文是协调Entity Framework功能最主要的类。
在解决方案资源管理器中,右键单击BabyStore项目,然后创建一个名为“DAL”的文件夹。在该文件夹中创建一个名为“StoreContext.cs”的类,然后在该类中添加如下代码:
1 using BabyStore.Models; 2 using System.Data.Entity; 3 4 namespace BabyStore.DAL 5 { 6 public class StoreContext : DbContext 7 { 8 public DbSet<Product> Products { get; set; } 9 public DbSet<Category> Categories { get; set; } 10 } 11 }
上下文类继承自System.Data.Entity.DbContext类,通常情况下,一个数据库对应一个数据库上下文类,在某些比较复杂的项目中可能对应多个。每一个DbSet类型的属性被称之为实体集,通常对应数据库中的某一个表,比如Products属性对应数据库中的Products表。代码DbSet<Product>告诉Entity Framework使用Product类来表示Products表中的一行数据。
2.3 指定连接字符串
现在,我们已经有了数据库上下文和一些模型类,现在需要我们告诉Entity Framework如何连接到数据库。在Web.config文件的connectionString节点参加一个新的条目:
1 <connectionStrings> 2 <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-BabyStore-20161229112118.mdf;Initial Catalog=aspnet-BabyStore-20161229112118;Integrated Security=True" 3 providerName="System.Data.SqlClient" /> 4 <add name="StoreContext" connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\BabyStore.mdf;Initial Catalog=BabyStore;Integrated Security=True" 5 providerName="System.Data.SqlClient" /> 6 </connectionStrings>
这个条目告诉Entity Framework要连接的是项目App_Data文件夹中的BabyStore.mdf数据库。我们选择在这个文件夹中存储数据库是因为它能够跟随项目一起被复制。AttachDbFilename=|DataDirectory|\BabyStore.mdf;指定在App_Data文件夹中创建数据库。
一个可选的指定连接字符串的条目是Data Source=(LocalDB)\MSSQLLocalDB;Initial Catalog=BabyStore.mdf;Integrated Security=True,它指定在用户文件夹(通常在Windows系统中是C:\Users\User)中创建数据库。
在connectionString节点中存在的其他条目是在我们创建工程时自动创建的,之所以会自动创建这个数据库是因为我们在身份验证选项中选择了“个人用户账户”。我们将在本书的后续章节讨论这个问题。
值得注意的是,我们可以不在web.config文件中定义连接字符串,如果这样做的话,Entity Framework将会使用基于上下文类的默认设置。
注意:确保使用的是项目根目录下的Web.config文件,而不是Views文件夹中的Web.config文件。
2.4 添加控制器和视图
现在我们需要添加一些控制器和视图来管理和显式我们的产品和分类数据。
2.4.1 添加分类控制器和视图
1、从Visual Studio菜单中,点击【生成】->【生成解决方案】来生成解决方案。
2、右击Controllers文件夹,然后选择【添加】->【控制器】。
3、在“添加基架”窗体中,选择“包含视图的MVC 5 控制器(使用 Entity Framework)”选项,如图2-1所示。
图2-1:使用Entity Framework创建控制器和视图
4、点击“添加”按钮,然后在“添加控制器”窗口中选择下列选项:
- 模型类:Category
- 数据上下文类:StoreContext
- 确保生成视图、引用脚本库和使用布局页选项被勾选上
- 保留控制器名称设置为CategoriesController(全部详情参见图2-2)
图2-2:添加新分类控制器的选项
5、点击“添加”按钮,则会在Controllers文件夹中创建一个CategoriesController类,与之相关的视图会创建在Views\Categories文件夹中。
2.4.2 检查CategoriesController类和方法
新搭建的CategoriesController.cs文件包含对分类执行CRUD(新建、查询、更新和删除)操作的多个方法。
代码private StoreContext db = new StoreContext();初始化了控制器要使用的一个上下文对象。该对象在控制器的整个生命周期中都可使用,在控制器的Dispose方法被调用时释放。
CategoriesController包含下列方法。
Inex方法用于返回所有分类的列表给Views\Categories\Index.cshtml视图:
1 // GET: Categories 2 public ActionResult Index() 3 { 4 return View(db.Categories.ToList()); 5 }
Details方法基于id参数从数据库查询某个分类信息,就像我们在第1章看到的那样,系统使用路由系统将URL中的id参数传递给该方法的id参数。
1 // GET: Categories/Details/5 2 public ActionResult Details(int? id) 3 { 4 if (id == null) 5 { 6 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 7 } 8 Category category = db.Categories.Find(id); 9 if (category == null) 10 { 11 return HttpNotFound(); 12 } 13 return View(category); 14 }
Create方法的GET版本只是简单地返回Create视图。第一次看到这个方法可能有点陌生,它的含义是返回一个视图,在该视图中显示一个空的HTML表单,用于创建一个新的分类。
1 // GET: Categories/Create 2 public ActionResult Create() 3 { 4 return View(); 5 }
Create方法的另一个版本使用HTTP POST请求。该方法在用户提交创建(Create)视图的表单时被调用。它带有一个Category类型的参数,并将该参数表示的Category对象添加到数据库中。如果该方法执行成功则返回到索引(Index)视图,否则,它会重新加载创建(Create)视图。
1 // POST: Categories/Create 2 // 为了防止“过多发布”攻击,请启用要绑定到的特定属性,有关 3 // 详细信息,请参阅 http://go.microsoft.com/fwlink/?LinkId=317598。 4 [HttpPost] 5 [ValidateAntiForgeryToken] 6 public ActionResult Create([Bind(Include = "ID,Name")] Category category) 7 { 8 if (ModelState.IsValid) 9 { 10 db.Categories.Add(category); 11 db.SaveChanges(); 12 return RedirectToAction("Index"); 13 } 14 15 return View(category); 16 }
因为该方法是一个HTTP POST,它包含一些额外的代码:
- [HttpPost]特性告诉控制器当调用Create动作方法的请求是一个POST请求时,使用该方法,而不使用其他重载的Create方法。
- [ValidateAntiForgeryToken]确保Token通过HTML表单传递,从而验证请求。这样做的目的是确保请求确确实实地是来自于我们所期望的表单,以防止跨站请求伪造。简单来说,跨站请求伪造就是来自于其他站点的表单请求伪装成我们自己站点的请求,从而执行带有恶意目的的操作。
- 参数([Bind(Include = "ID,Name")] Category category)告诉方法在添加一个新的分类时只包含ID和Name属性。Bind特性使用Include属性创建了一个安全属性列表,只有该列表中的属性允许被修改,从而防止overposting攻击。但是,就像我们后面讨论的那样,它不像我们所期望的那样工作。因此,我们需要使用一种不同的方法来处理一些值可能为空的编辑和新建操作。举一个overposting的例子,考虑下面一种场景,当用户提交一个产品订单的时候,该产品的价格也被提交,overposting攻击会尝试通过购买一个较低价格的商品来修改已经被提交的产品价格。
Edit方法的GET版本包含的代码和Details方法的代码一样。该方法根据ID找到一个分类,然后将该分类数据传递给视图。该视图按一定的格式显示分类信息,并允许进行编辑。
1 // GET: Categories/Edit/5 2 public ActionResult Edit(int? id) 3 { 4 if (id == null) 5 { 6 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 7 } 8 Category category = db.Categories.Find(id); 9 if (category == null) 10 { 11 return HttpNotFound(); 12 } 13 return View(category); 14 }
Edit方法的POST版本和Create方法的POST版本很类似。它包含一行额外的代码用于在将实体保存到数据库之前检查该实体是否被修改过,如果方法执行成功,则返回到索引(Index)视图,否则重新显示编辑(Edit)视图。
1 // POST: Categories/Edit/5 2 // 为了防止“过多发布”攻击,请启用要绑定到的特定属性,有关 3 // 详细信息,请参阅 http://go.microsoft.com/fwlink/?LinkId=317598。 4 [HttpPost] 5 [ValidateAntiForgeryToken] 6 public ActionResult Edit([Bind(Include = "ID,Name")] Category category) 7 { 8 if (ModelState.IsValid) 9 { 10 db.Entry(category).State = EntityState.Modified; 11 db.SaveChanges(); 12 return RedirectToAction("Index"); 13 } 14 return View(category); 15 }
Delete方法也有两个版本。ASP.NET MVC基架将要删除的详细信息展示给用户,在用户真正提交删除请求之前进行确认。我们在这先列出GET版本的Delete方法,我们会注意到该方法和Details方法十分类型,使用ID找到一个分类,并将该分类数据传递给视图。
1 // GET: Categories/Delete/5 2 public ActionResult Delete(int? id) 3 { 4 if (id == null) 5 { 6 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 7 } 8 Category category = db.Categories.Find(id); 9 if (category == null) 10 { 11 return HttpNotFound(); 12 } 13 return View(category); 14 }
Delete方法的POST版本执行一个防伪检查,它首先利用ID找到一个分类,然后移除它,最后保存对数据库的修改。
1 // POST: Categories/Delete/5 2 [HttpPost, ActionName("Delete")] 3 [ValidateAntiForgeryToken] 4 public ActionResult DeleteConfirmed(int id) 5 { 6 Category category = db.Categories.Find(id); 7 db.Categories.Remove(category); 8 db.SaveChanges(); 9 return RedirectToAction("Index"); 10 }
由于产品实体包含了一个对分类实体的外键引用,自动生成的Delete方法不能正确的工作。我们将在第4章学习如何修正该问题。
注意:有多个原因导致ASP.NET采取了不允许GET请求更新数据库的方式,也有许多关于这样做的安全性的评论和争论。但是,这样做的最主要的原因是搜索引擎爬行器会爬行我们站点中的所有公开的超链接,如果这些链接中包含未认证即可删除记录的链接,那么,可能会导致数据库中的相关数据被删除。稍后我们会给编辑分类添加安全机制,因此,这将变成一个有争议的问题。
2.4.3 检查分类视图
与分类相关的视图可在\Views\Categories文件夹中找到。每一个CRUD动作(详情、创建、编辑和删除)都有一个视图,索引(Index)视图用于显示所有分类的一个列表。
2.4.3.1 分类的索引(Index)视图
自动生成的Categories\Views\Index.cshtml视图文件如下所示:
1 @model IEnumerable<BabyStore.Models.Category> 2 3 @{ 4 ViewBag.Title = "Index"; 5 } 6 7 <h2>Index</h2> 8 9 <p> 10 @Html.ActionLink("Create New", "Create") 11 </p> 12 <table class="table"> 13 <tr> 14 <th> 15 @Html.DisplayNameFor(model => model.Name) 16 </th> 17 <th></th> 18 </tr> 19 20 @foreach (var item in Model) 21 { 22 <tr> 23 <td> 24 @Html.DisplayFor(modelItem => item.Name) 25 </td> 26 <td> 27 @Html.ActionLink("Edit", "Edit", new { id = item.ID }) | 28 @Html.ActionLink("Details", "Details", new { id = item.ID }) | 29 @Html.ActionLink("Delete", "Delete", new { id = item.ID }) 30 </td> 31 </tr> 32 } 33 34 </table>
在这个视图中的各个条目如下所示:
- @model IEnumerable<BabyStore.Models.Category>是该视图所基于的模型。CategoriesController控制器类的Index方法向索引(Index)视图传递了一个分类列表。在这个例子中,视图需要向用户显示分类信息列表,因此模型指定为实现了IEnumerable接口的分类集合。
- 当前页的title属性使用下列代码进行了设置:
1 @{ 2 ViewBag.Title = "Index"; 3 }
- @Html.ActionLink("Create New", "Create")创建了一个文本为“Create New”的、链接到创建(Create)视图的超链接。这是一个使用HTML辅助器的例子,ASP.NET MVC通过使用这些辅助器来渲染各种不同的数据驱动的HTML元素。
- @Html.DisplayNameFor(model => model.Name)显示模型中指定属性的值。在这个例子中,它显示了分类的Name属性的的值。
- 然后代码循环遍历模型中包含的每一个分类,并显示每个分类的Name属性所包含的值,后面紧跟三个链接:Edit、Details和Delete(如图2-3)。ActionLink方法的第三个参数用于向链接打开的视图提供分类的id。
1 @foreach (var item in Model) 2 { 3 <tr> 4 <td> 5 @Html.DisplayFor(modelItem => item.Name) 6 </td> 7 <td> 8 @Html.ActionLink("Edit", "Edit", new { id = item.ID }) | 9 @Html.ActionLink("Details", "Details", new { id = item.ID }) | 10 @Html.ActionLink("Delete", "Delete", new { id = item.ID }) 11 </td> 12 </tr> 13 }
图2-3:分类索引(Index)视图生成的HTML页面(包括示例数据)
译者注:示例数据的添加我们后面会讲到,这儿有点超前。
.4.3.2 分类的详情(Details)视图
由基架生成的Views\Categories\Details.cshtml视图文件如下所示:
1 @model BabyStore.Models.Category 2 3 @{ 4 ViewBag.Title = "Details"; 5 } 6 7 <h2>Details</h2> 8 9 <div> 10 <h4>Category</h4> 11 <hr /> 12 <dl class="dl-horizontal"> 13 <dt> 14 @Html.DisplayNameFor(model => model.Name) 15 </dt> 16 17 <dd> 18 @Html.DisplayFor(model => model.Name) 19 </dd> 20 21 </dl> 22 </div> 23 <p> 24 @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) | 25 @Html.ActionLink("Back to List", "Index") 26 </p>
这段代码比索引(Index)视图简单,它只显示单一实体。文件的第一行代码所指定的模型是一个单一实体而不是集合。
@model BabyStore.Models.Category
在该视图中使用了与索引(Index)视图中一样的HTML辅助器,但这次不需要使用循环,因为只有一个单一实体,如图2-4所示。
图2-4:分类详细(Details)视图生成的HTML页面
2.4.3.3 分类的创建(Create)视图
创建(Create)视图显示了一个空表单以允许我们创建一个分类。为了生成一个HTML表单,该视图实现了一些在索引(Index)视图和详情(Details)视图中没有包括的新特性。这个表单使用POST请求提交,该请求会被POST版本的Create方法处理。这个视图自动生成的代码如下所示:
1 @model BabyStore.Models.Category 2 3 @{ 4 ViewBag.Title = "Create"; 5 } 6 7 <h2>Create</h2> 8 9 @using (Html.BeginForm()) 10 { 11 @Html.AntiForgeryToken() 12 13 <div class="form-horizontal"> 14 <h4>Category</h4> 15 <hr /> 16 @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 17 <div class="form-group"> 18 @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) 19 <div class="col-md-10"> 20 @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) 21 @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) 22 </div> 23 </div> 24 25 <div class="form-group"> 26 <div class="col-md-offset-2 col-md-10"> 27 <input type="submit" value="Create" class="btn btn-default" /> 28 </div> 29 </div> 30 </div> 31 } 32 33 <div> 34 @Html.ActionLink("Back to List", "Index") 35 </div> 36 37 @section Scripts { 38 @Scripts.Render("~/bundles/jqueryval") 39 }
图2-5显示了所生成的HTML页面。下面是上述代码中的一些关键点:
- 第一个新特性使用的代码是@Using(Html.BeginForm()),该代码行告诉视图将这个using语句中的所有代码都包裹在HTML表单中。
- @Html.AntiForgeryToken()生成一个anti-forgery token,被匹配的POST版本的Create方法用于检查(使用[ValidateAntiForgeryToken]特性)。
- @Html.ValidationSummary(true, "", new { @class = "text-danger" })是另一个辅助器,用于显示一个错误摘要(导致表单无效的任何原因)。第一个参数告诉摘要排除任何属性错误,只显示模型级别的错误,第三个参数new { @class = "text-danger" }用于使用Bootstrap的text-danger CSS类样式化错误信息(这是一个红色的文本,更多关于Bootstrap和CSS的知识包含在本书后续章节)。
- @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })创建了一个新的HTML label元素,该label元素与随后的HTML输入控件(分类的Name属性)相关联。
- @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })是另一个HTML辅助器方法,可以根据指定属性的数据格式来显示正确的HTML输入元素。在这个例子中,属性是Name,因此EditorFor方法尝试着显示正确的HTML元素类型以允许用户编辑字符串。在这个情况下,它在HTML表单中创建了一个文本框元素,所以用户可以输入分类的名称。
- 在这个视图中的最后一个新HTML辅助器是@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })。这段代码对属性添加了特殊的验证消息,如果用户输入的属性值违法了在程序中设置的验证规则,则会触发一个错误。当前,我们还没有设置规则,但是我们将在第4章学习如何设置它。
- 在这个视图中还有一个额外的部分是在索引(Index)视图和详情(Details)视图文件中没有的,它用于包含验证所需的JavaScript文件(更多知识点将在第4章讲述):
1 @section Scripts { 2 @Scripts.Render("~/bundles/jqueryval") 3 }
图2-5:分类的创建(Create)视图所生成的HTML页面,这个页面包含一个用于提交新分类信息的HTML表单
2.4.3.4 分类的编辑(Edit)视图
分类的编辑视图显示了一个允许用户编辑分类信息的HTML表单,分类信息由CategoriesController控制器类的Edit方法(GET版本)传递给该视图。这个视图非常类似于创建(Create)视图。该视图自动生成的代码如下所示:
1 @model BabyStore.Models.Category 2 3 @{ 4 ViewBag.Title = "Edit"; 5 } 6 7 <h2>Edit</h2> 8 9 @using (Html.BeginForm()) 10 { 11 @Html.AntiForgeryToken() 12 13 <div class="form-horizontal"> 14 <h4>Category</h4> 15 <hr /> 16 @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 17 @Html.HiddenFor(model => model.ID) 18 19 <div class="form-group"> 20 @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) 21 <div class="col-md-10"> 22 @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) 23 @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) 24 </div> 25 </div> 26 27 <div class="form-group"> 28 <div class="col-md-offset-2 col-md-10"> 29 <input type="submit" value="Save" class="btn btn-default" /> 30 </div> 31 </div> 32 </div> 33 } 34 35 <div> 36 @Html.ActionLink("Back to List", "Index") 37 </div> 38 39 @section Scripts { 40 @Scripts.Render("~/bundles/jqueryval") 41 }
图2-6显示了所生成的编辑页面。在这个视图中唯一的一个新的HTML辅助器方法是@Html.HiddenFor(model => model.ID)。该语句创建了一个包含分类ID的隐藏的HTML输入元素,用于CategoriesController控制器类的Edit方法(POST版本)的Bind元素:public ActionResult Edit([Bind(Include = "ID,Name")] Category category)。
图2-6:分类的编辑(Edit)视图所生成的HTML页面,分类的当前名称被预先填充在输入框中。
2.4.3.5 分类的删除(Delete)视图
删除(Delete)视图和详情(Details)视图很类似,而且也包含一个HTML表单,用于提交到CategoriesController控制器类的Delete方法(POST版本)。除了前面我们所检查的视图所包含的内容之外,该视图没有包含其他任何新的特性。自动生成的代码如下所示,该视图所生成的HTML如图2-7所示:
1 @model BabyStore.Models.Category 2 3 @{ 4 ViewBag.Title = "Delete"; 5 } 6 7 <h2>Delete</h2> 8 9 <h3>Are you sure you want to delete this?</h3> 10 <div> 11 <h4>Category</h4> 12 <hr /> 13 <dl class="dl-horizontal"> 14 <dt> 15 @Html.DisplayNameFor(model => model.Name) 16 </dt> 17 18 <dd> 19 @Html.DisplayFor(model => model.Name) 20 </dd> 21 22 </dl> 23 24 @using (Html.BeginForm()) 25 { 26 @Html.AntiForgeryToken() 27 28 <div class="form-actions no-color"> 29 <input type="submit" value="Delete" class="btn btn-default" /> | 30 @Html.ActionLink("Back to List", "Index") 31 </div> 32 } 33 </div>
2.4.4 添加产品控制器和视图
1、右击Controllers文件夹,然后选择【添加】->【控制器】。
2、在“添加基架”窗体中,选择“包含视图的 MVC 5 控制器(使用 Entity Framework)”选项(如图2-1)。
3、点击“添加”按钮,然后在“添加控制器”窗口中选择下列选项:
- 模型类:Product
- 数据上下文类:StoreContext
- 确保生成视图、引用脚本库和使用布局页选项被勾选上
- 保留控制器名称设置为ProductsController(全部详情参见图2-8)
图2-8:添加新产品控制器的选项
4、点击“添加”按钮,则会在Controllers文件夹中创建一个ProductsController类,与之相关的视图会创建在Views\Products文件夹中。
2.4.4.1 检查产品控制器和视图
ProductsController控制器类和与之相关的视图和前面所讲述的CategoriesController类非常相似,我们就不再详细描述了。然而,该视图包含了一个非常重要的新功能,应用程序需要提供一种方式关联产品和分类,在该视图中所采用的方式是使用select元素显示一个下拉列表,以便用户在创建产品和编辑产品时可以选择产品所对应的分类。
在控制器中,实现这个功能的代码可以在Edit方法(GET版本和POST版本)和Create方法(POST版本)中找到:
ViewBag.CategoryID = new SelectList(db.Categories, "ID", "Name", product.CategoryID);
在Create方法的GET版本中,与之相类似的代码如下所示:
ViewBag.CategoryID = new SelectList(db.Categories, "ID", "Name");
这段代码将一个条目赋值给了ViewBage的CategoryID属性。这个条目是一个SelectList对象,该对象包括数据库中的所有分类,每一个条目使用Name属性作为要显示的文本,使用ID属性作为其值。可选的第四个参数决定在select列表中预先选定的条目。举个例子,如果第四个参数product.CategoryID设置为2,那么视图中的分类下拉列表中将会预先选定分类Toys。图2-9显示了它在视图中的样子。
在视图中显示HTML的select元素使用下列HTML辅助器方法:@Html.DropDownList("CategoryID", null, htmlAttributes: new { @class = "form-control" })。
这段代码基于ViewBag.CategoryID属性生成一个HTML元素,并将该元素的CSS class属性设置为form-control。在DropDownList辅助器方法中,如果第一个字符串参数的值匹配ViewBag属性的名字,它将会自动使用这个值,而不会再指定一个到ViewBag的引用。
图2-9:使用product.CategoryID参数在下拉列表中预先选定的元素值
2.5 使用新的产品和分类视图
我们不期望用户在浏览器地址栏中手动输入URL来导航到我们新建的视图,因此,我们需要更新主站点的导航栏:
1、打开Views\Shared\_Layout.cshtml文件。
2、在代码<li>@Html.ActionLink("联系方式", "Contact", "Home")</li>的下面添加分类和产品的索引(Index)页面:
1 <div class="navbar-collapse collapse"> 2 <ul class="nav navbar-nav"> 3 <li>@Html.ActionLink("主页", "Index", "Home")</li> 4 <li>@Html.ActionLink("关于", "About", "Home")</li> 5 <li>@Html.ActionLink("联系方式", "Contact", "Home")</li> 6 <li>@Html.ActionLink("分类", "Index", "Categories")</li> 7 <li>@Html.ActionLink("产品", "Index", "Products")</li> 8 </ul> 9 @Html.Partial("_LoginPartial") 10 </div>
3、点击【调试】->【开始执行(不调试)】,Web站点将会启动。点击“分类”链接的时候将会发生两件事:
- 分类的索引(Index)视图将会出现。可能该视图不包含任何数据,因为我们还没有在数据库中添加数据(如图2-10)。
- Entity Framework会使用Code First模式,基于我们的模型类创建BabyStore数据库。为了查看该数据库,在Visual Studio中打开SQL Server对象资源管理器。如果SQL Server对象资源管理器没有出现,则从主菜单中点击【视图】->【SQL Server对象资源管理器】。
图2-10:分类的索引(Index)页面
2.5.1 检查新创建的BabyStore数据库
要看出数据库中的新列,在SQL Server对象资源管理器中展开以下节点:SQL Server>(localdb)\MSSQLLocalDB>数据库>BabyStore>表>dbo.Categories>列。同时也将dbo.Products>列展开,如图2-11。
图2-11:SQL Server对象资源管理器中显示的最初的BabyStore数据库,分类和产品表的列依次展开,并显示出每个列的数据类型
在数据库中列出的每个列都和模型类的属性相匹配。表名被默认设置为复数形式。Categories表包含ID和Name列。Products表包含ID、Name、Description、Price列以及一个外键列CategoryID。每个列的数据类型和模型类中的属性的数据类型相匹配。
要想更加详细地查看每个表的详情,右击表,然后在菜单中选择【视图设计器】。使用视图设计器,我们可以更加详细地查看表中的每个列,以及外键。在图2-12中,我们可以看到Products表的设计以及T-SQL部分,T-SQL部分的第8行代码用于设置外键约束,表面该外键列引用Categories表中的ID列。
图2-12:带有外键约束的Products表的设计器
2.5.2 使用视图添加一些数据
为了测试新视图的功能,我们点击分类的索引(Index)视图中的Create New链接来添加一些数据。添加三个分类:Clothes、Toys和Feeding。当我们完成这些操作后,分类的索引(Index)页面如图2-13所示。
图2-13:带有三个测试分类数据的分类索引(Index)页面
新添加的分类信息已经被添加到数据库中,因为当用户点击分类的创建(Create)页面中的Create按钮时,CategoriesController类中的Create方法(POST版本)被调用,然后将新分类信息保存到数据库中。
点击产品链接,然后再点击Create New链接来添加一些产品数据。添加的产品数据如表2-1所示。
表2-1:添加到站点的产品信息
每一次点击产品的创建(Create)页面的Create按钮,ProductsController类的Create方法(POST版本)都会被调用,从而保存产品数据到数据库中。
一旦完成添加,产品的索引(Index)页面应如图2-14所示。数据现在也别保存到数据库中,如图2-15。要查看数据库中的数据,可以在SQL Server对象资源管理器中右键单击dbo.Products表,然后从菜单中选择【查看数据】菜单项。
图2-14:带有产品数据的产品索引(Index)页面
图2-15:查看数据库中的Products表数据
注意:在视图中自动生成的默认的表头和标签对于用户的使用不太友好,因此作者在随后的截图中对他们进行了修正。如果我们想自己进行修正,可以参照第二章的源码,该源码可以从www.Apress.com下载。作者没有在本书中对代码进行修正,因为它们太过重复和繁琐。
2.5.3 使用数据注解的方法更改分类和产品的Name属性的显示
产品的索引(Index)页面包含两个名为Name的标题,如图2-16所示。这是因为在视图中使用了@Html.DisplayNameFor(model => model.Category.Name)代码来显示分类的Name属性,紧接着也使用了该方法来显示产品的Name属性。在产品的详情(Details)页面也有同样的问题,这造成了用户使用的困惑。
图5-16:产品的索引(Index)页面中的两个Name表头
为了解决这个问题,我们可以使用一个称之为数据注解的ASP.NET特性在分类和产品的模型类的Name属性上添加一个Display特性。
在Models\Category.cs文件中添加如下高亮显示的代码:
1 using System.Collections.Generic; 2 using System.ComponentModel.DataAnnotations; 3 4 namespace BabyStore.Models 5 { 6 public class Category 7 { 8 public int ID { get; set; } 9 10 [Display(Name = "Category Name")] 11 public string Name { get; set; } 12 public virtual ICollection<Product> Products { get; set; } 13 } 14 }
[Display(Name = "Category Name")]告诉MVC框架在显示属性名称标签的时候,不使用Name而是使用Category Name。
按同样的方式修改Models\Product.cs文件中的代码:
1 using System.ComponentModel.DataAnnotations; 2 3 namespace BabyStore.Models 4 { 5 public class Product 6 { 7 public int ID { get; set; } 8 [Display(Name = "Product Name")] 9 public string Name { get; set; } 10 public string Description { get; set; } 11 public decimal Price { get; set; } 12 public int? CategoryID { get; set; } 13 public virtual Category Category { get; set; } 14 } 15 }
从菜单栏中选择【生成解决方案】,然后再选择【调试】->【开始执行(不调试)】菜单项来启动应用程序。点击产品链接打开产品索引(Index)页面,我们会看到两个Name标题现在变成了Category Name和Product Name,如图2-17所示。
图2-17:产品索引(Index)页面显示的数据注解的显示名称
在类中使用数据注解可以使得我们的代码更易维护,因为对于属性名称的显示只需要在一个地方进行控制即可。我们也可以在视图中修改显示名称,但这涉及到两个文件,因此变得难以维护,此外,对于我们使用该属性创建的视图,未来都需要被更新。
2.5.3.1 使用MetaDataType将数据注解分割为另一个文件
一些开发人员喜欢模型类尽可能地简单明了,因此,不愿意对模型类添加数据注解。这可以使用MetaDataType类来完成这种需求。
在Models文件夹中添加一个名为ProductMetaData.cs的文件,然后修改其代码如下所示:
1 using System.ComponentModel.DataAnnotations; 2 3 namespace BabyStore.Models 4 { 5 [MetadataType(typeof(ProductMetaData))] 6 public partial class Product 7 { 8 } 9 10 public class ProductMetaData 11 { 12 [Display(Name = "Product Name")] 13 public string Name; 14 } 15 }
现在将Product类声明成了一个分部类,这意味着将该类分割为了多个文件。数据注解[MetadataType(typeof(ProductMetaData))]用于告诉.NET要将来自于ProductMetaData类的元数据应用于Product类。
将Product类修改成原来的状态,但是将它声明为一个分部类,以便与声明在ProductMetaData.cs文件中的Product类声明进行合并。
1 namespace BabyStore.Models 2 { 3 public partial class Product 4 { 5 public int ID { get; set; } 6 public string Name { get; set; } 7 public string Description { get; set; } 8 public decimal Price { get; set; } 9 public int? CategoryID { get; set; } 10 public virtual Category Category { get; set; } 11 } 12 }
修正后的代码所产生的结果将和图2-17显示的效果一样。然而,使用这种编码方法,除了将Product类声明为一个分部类之外,没有对Product类进行任何修改。当我们使用一些自动生成的类,而我们又不想对这些类进行修改时,这是一种非常有用的策略,比如,当我们使用Entity Framework的DataBase First模式时。在本书中,我们没有涵盖Entity Framework的Database First相关知识,但是我们将讲述另外一种场景:对一个已经存在的数据库使用Code First。
2.6 简单查询:按照字母顺序对分类进行排序
在分类的索引(Index)视图中所显示的分类目前是按照ID来进行排序的,我们可以修改成按照字母顺序对分类的名称进行排序。
实现这个功能十分简单,打开Controllers\CategoriesController.cs文件,然后将Index方法修改成如下代码:
1 // GET: Categories 2 public ActionResult Index() 3 { 4 return View(db.Categories.OrderBy(c => c.Name).ToList()); 5 }
点击【调试】->【开始执行(不调试)】启动应用程序,然后点击分类链接。现在分类将会按照字母顺序对产品的名称进行排序,如图2-18所示。
图2-18:按照字母顺序对分类名称进行排序
这段代码使用LINQ方法语法(method syntax)来指定对哪个列进行排序。lambda表达式用于指定要排序的列是Name列。这段代码将返回一个排序过的分类列表给视图显示。LINQ表示Language-Integrated Query,它是内置在.NET框架中的一个查询语言。使用LINQ方法语法(method syntax)意味着所创建的查询可以使用点“.”快速地将多个方法进行链接。一个可替换方法语法(method syntax)的方式是使用查询语法(query syntax),我们将在第3章给出一个编写的一个比较复杂的查询例子。方法语法(method syntax)在外观上更像SQL的语法,对于比较复杂的查询可以让我们更容易理解。但是,对于稍短的查询它显得就比较冗长。
lambda表达式是匿名方法,这些方法可用于创建委托。简单来说,lambda表达式可以让我们创建一个表达式,该表达式的lambda操作符(=>)左边的值是输入参数,右边的是要计算的表达式和返回值。考虑上面我们输入的lambda表达式,它带有一个Category类型的输入参数,然后返回分类的Name属性。因此,简单的说,它表述的意思就是按照分类的Name属性排序。
在本书中,我们没有详细讲述LINQ或lambda表达式。如果你想对其了解的更多,我们建议你读下Andrew Troelsen编写的一本非常优秀的图书:Pro C#(中文图书名为:精通C#)。
2.7 按照类别过滤产品:使用导航属性和Include搜索相关实体
我们已经看到如何创建一个非常基本的页面用于显示不同实体的列表。现在,我们将添加一些有用的功能从而让这些列表进行交互。我们将使用分类列表中被选择的值来过滤产品列表。为了达到此目的,我们需要修改下列代码:
- 修改ProductsController的Index方法,使它能够接收一个参数,该参数表示选择的分类,并且返回一个属于该分类的产品列表。
- 将分类索引(Index)页中的文本列表修改成超链接列表,该超链接的目标是ProductsController的Index方法。
第一处改动是ProductsController的Index方法,如下所示:
1 public ActionResult Index(string category) 2 { 3 var products = db.Products.Include(p => p.Category); 4 if (!string.IsNullOrEmpty(category)) 5 { 6 products = products.Where(p => p.Category.Name == category); 7 } 8 return View(products.ToList()); 9 }
这段代码给Index方法添加了一个名为category的字符串参数。if语句用于检查category参数是否为空,如果该参数不为空,将会使用Product类的导航属性Category来对产品进行过滤,代码为:products = products.Where(p => p.Category.Name == category);。
下面一行代码所展示的Include方法是一个使用预先加载(eager loading)的例子:
1 var products = db.Products.Include(p => p.Category);
它告诉Entity Framework去执行一个单一查询,检索出所有的产品和这些产品相关的分类信息。预先加载(eager loading)会导致一个SQL连接查询,它一次检索出所需的所有数据。我们可以忽略Include方法,这时,Entity Framework将会使用延迟加载(lazy loading),这将涉及多个查询而不是一个单一的连接查询。
选择使用那一种加载方法有一些性能上的差异。预先加载对数据库进行一次查询就可以获得结果,但是,如果使用比较复杂的连接语句会导致性能下降。延迟加载会对数据库进行多次查询才能获得所需数据。在这儿,我们使用预先加载是因为连接语句比较简单,同时,我们需要加载相关的分类信息以便对齐进行搜索。
products变量使用Where方法匹配产品的分类名称与传递进来的category参数值一致的那些产品。这可能有点小题大做,同时,我们可能也会迷惑为什么我们不使用CategoryID而是使用字符串来给Index方法传值。关键原因在于,当我们使用路由时,使用分类名称更加有意义。我们将在本书后续章节讲述相关知识。
这是一个非常好的例子,它展示了导航属性是非常有用和强大的。由于使用了Product类中的导航属性,我们可以使用较少的代码来搜索两个相关的实体。如果我们想根据分类名称来匹配产品,但是不使用导航属性的话,我们必须在ProductsController中加载分类实体,但是按照惯例,ProductsController只对产品进行管理。
为了演示新方法达到的效果,我们启动站点,然后导航到产品的索引(Index)页面。现在我们在URL后面最佳代码?category=clothes。现在,产品列表将会被匹配的项目过滤,如图2-19。
图2-19:使用URL地址栏按照clothes分类过滤产品
在查询字符串中的任何参数都会自动匹配目标方法的参数。因此,在这个例子中,URL中的?category=clothes匹配ProductsController中的Index方法的category参数,此时,该参数的值为clothes。
注意:对于刚刚开始使用Entity Framework的编码人员来说,最常见的错误是在错误的地方使用了ToList()方法。在一个方法中,LINQ通常用于创建一个查询,但是不执行这个查询!查询只在ToList()方法被调用时才被执行。初级编码人员经常在他们方法的开始就使用ToList()方法,这通常导致比较多的记录(通常是所有的记录)从数据库中检索出来,其中有些记录并不是所需要的,从而对性能产生影响。所有这些记录都被保存在内存中,并作为内存中的列表进行处理,这通常是不可取的,它会使站点的想能大幅降低。作为选择,我们甚至都不用调用ToList()方法,当加载视图时,查询会被执行。这个话题被称之为延迟执行,延迟执行归功于查询只有在ToList()方法被调用时才会执行。
为了完成根据分类过滤产品的功能,我们需要修改分类的索引(Index)页面中的产品列表,将其更改为链接到ProductsController的Index方法的超链接。
为了将分类更改为超链接,我们修改Views\Categories\Index.cshtml文件,将其文件中的@Html.DisplayFor(modelItem => item.Name)修改为:
1 @foreach (var item in Model) 2 { 3 <tr> 4 <td> 5 @*@Html.DisplayFor(modelItem => item.Name)*@ 6 @Html.ActionLink(item.Name, "Index", "Products", new { category = item.Name }, null) 7 </td> 8 <td> 9 @Html.ActionLink("Edit", "Edit", new { id = item.ID }) | 10 @Html.ActionLink("Details", "Details", new { id = item.ID }) | 11 @Html.ActionLink("Delete", "Delete", new { id = item.ID }) 12 </td> 13 </tr> 14 }
这段代码使用HTML的ActionLink辅助器方法生成一个超链接,该链接的显示文本是分类的名称,目标是ProductsController的Index方法。第四个参数是路由参数,用于将分类的Name属性值赋值给category参数,该参数会跟随URL传递给目标方法参数。使用这种方法和我们使用手动方式的效果一样,都会在URL的后面追加category=caegoryname样式的参数。
通过上述的修改,分类的索引(Index)页面现在包含了链接到ProductsController的Index方法的超链接,如图2-20所示。
点击每一个链接,现在都会打开产品的索引(Index)页面,在该页面中只显示了与该分类相关的产品信息。
图2-20:带有超链接的、用于过滤产品的分类索引(Index)页面。标红的部分是clothes链接生成的URL。
2.8 小节
在这一章中,我们学习了如何创建模型类以及如何根据模型类生成数据库。我们还学习了如何指定数据库连接字符串,以及如何创建数据库上下文。这些类以及我们的模型类可用于创建控制器和视图,我们还创建和填充了数据库。
数据库创建完毕之后,我们还学习了如何检查它以及如何修改视图以修正与基架相关的问题。从这开始,本章余下的部分主要讲述了如何使用分类过滤产品,如何使用导航属性以及如何从视图链接到不同的动作方法。