接下来是关于SportsStore的后台管理功能,也就是通常的CRUD操作。
首先添加一个AdminController,代码如下:
View Code
using System.Web.Mvc; using SportsStore.Domain.Abstract; namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } } }
我们通过一个List page来展示已有的products,接着在AdminController里面添加一个Action:
View Code
using System.Web.Mvc; using SportsStore.Domain.Abstract; namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } public ViewResult Index() { return View(repository.Products); } } }
为我们的后台管理界面创建一个布局_AdminLayout.cshtml,在 Views/Shared上右键添加新项MVC 3 Layout Page
View Code
<!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/Admin.css")" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> </body> </html>
接着添加一个样式表单Admin.css,如下所示:
View Code
BODY, TD { font-family: Segoe UI, Verdana } H1 { padding: .5em; padding-top: 0; font-weight: bold; font-size: 1.5em; border-bottom: 2px solid gray; } DIV#content { padding: .9em; } TABLE.Grid TD, TABLE.Grid TH { border-bottom: 1px dotted gray; text-align:left; } TABLE.Grid { border-collapse: collapse; width:100%; } TABLE.Grid TH.NumericCol, Table.Grid TD.NumericCol { text-align: right; padding-right: 1em; } FORM {margin-bottom: 0px; } DIV.Message { background: gray; color:White; padding: .2em; margin-top:.25em; } .field-validation-error { color: red; display: block; } .field-validation-valid { display: none; } .input-validation-error { border: 1px solid red; background-color: #ffeeee; } .validation-summary-errors { font-weight: bold; color: red; } .validation-summary-valid { display: none; }
添加Index视图:
View Code
@model IEnumerable<SportsStore.Domain.Entities.Product>@{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_AdminLayout.cshtml";}<h1> All Products</h1><table class="Grid"> <tr> <th> ID </th> <th> Name </th> <td> ShowImg </td> <th class="NumericCol"> Price </th> <th> Actions </th> </tr> @foreach (var item in Model) { <tr> <td>@item.ProductID </td> <td>@Html.ActionLink(item.Name, "Edit", new { item.ProductID }) </td> <td> <img src="/UploadImgs/@item.ImageURL" alt="img" id="@item.ProductID" width="100" height="100" /> </td> <td class="NumericCol">@item.Price.ToString("c") </td> <td> @using (Html.BeginForm("Delete", "Admin")) { @Html.Hidden("ProductID", item.ProductID) <input type="submit" value="Delete" /> } </td> </tr> }</table><p>@Html.ActionLink("Add a new product", "Create")</p>
这里对数据库有改动,需要增加一个字段ImageURL(varchar(255)),并且在我们的SportsStore.WebUI项目新添加一个文件夹UploadImgs用来存放上传的图片。
编辑Product
创建一个Edit Action
View Code
public ViewResult Edit(int productId) { Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); return View(product); }
创建Edit View,强类型的并引用了_AdminLayout.cshtml,
如下:
View Code
@model SportsStore.Domain.Entities.Product@{ ViewBag.Title = "Admin:Edit" + @Model.Name; Layout = "~/Views/Shared/_AdminLayout.cshtml";}<h2> Edit @Model.Name</h2>@using (Html.BeginForm("Edit", "Admin", FormMethod.Post, new { enctype = "multipart/form-data" })){ @Html.EditorForModel() <div class="editor-label"> Image</div> <div class="editor-field"> @if (Model.ImageURL == null) { @:None }else { <img width="150" height="150" alt="img" src="/UploadImgs/@Model.ImageURL" /> } <div> Upload new image:<input type="file" name="Image" /></div> </div> <input type="submit" value="Save" /> @Html.ActionLink("Cancel and return to list", "Index");}
如果运行程序,这里并不会显示ProductID,因为这里没有必要显示出来。实现这个效果需要在Product实体里面使用模型元数据(model metedata)。它能让我们应用Attributes到Product的属性上面来影响Html.EditorForModel()方法输出的布局。
如下:
View Code
public class Product {//通过这个属性来在隐藏ProductID [HiddenInput(DisplayValue = false)]public int ProductID { get; set; } [Required(ErrorMessage = "Please enter a product name")]public string Name { get; set; } [Required(ErrorMessage = "Please enter a description")] [DataType(DataType.MultilineText)]public string Description { get; set; } [Required] [Range(typeof(decimal), "0.01", "999999999999999", ErrorMessage = "Please enter a positive price")]public decimal Price { get; set; } [Required(ErrorMessage = "Please specify a category")]public string Category { get; set; } public string ImageURL { get; set; } }
HiddenInput特性让MVC框架将该属性作为隐藏表单元素呈现,DataType特性允许我们指定一个值怎样显示或编辑。如这里将Description显示为MultilineText(多行文本)
这里需要添加一个引用System.Web.Mvc。
接着添加一些样式到Admin.css里面,如下:
View Code
.editor-field { margin-bottom: .8em; } .editor-label { font-weight: bold; } .editor-label:after { content: ":" } .text-box { width: 25em; } .multi-line { height: 5em; font-family: Segoe UI, Verdana; }
这里看发现css里面的类名如.editor-field,我们并没有在View里面定义一个class属性,这个是MVC框架生成的,你可以查看页面源文件。而且ID都是属性名,这是模型绑定的结果,在前面的笔记有这些内容的。
为了编辑Product,我们需要添加相应的Repository和接口。在IProductsRepository里面添加一个方法:void SaveProduct(Product product);
接着实现该接口,如下:
View Code
public class ProductsRepository : IProductsRepository { ISqlMapper mapper = MapperHelper.Instance();public IList<Product> Products {get { return mapper.QueryForList<Product>("Product-Select", ""); } }public void SaveProduct(Product product) { mapper.Update("Product-Update", product); } public void AddProduct(Product product) { mapper.Insert("Product-Insert", product); } public void DeleteProduct(Product product) { mapper.Delete("Product-Delete", product); } }
初次路过的朋友可能会惊讶这里是什么东东,书中用的EF框架+MS SQL Server。我学着用的是iBatisnet+MySQL。如果你对iBatisnet不了解,可以先用下iBatisnet,上手还算比较容易。你也可以看看我前面笔记之十二,里面讲了我这里的使用情况。
这里我们需要在iBatisnet的映射文件Product.xml添加update语句,如下:
View Code
<?xml version="1.0" encoding="utf-8" ?><sqlMap namespace="Product" xmlns="http://ibatis.apache.org/mapping"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" > <alias> <typeAlias alias="Product" type="SportsStore.Domain.Entities.Product,SportsStore.Domain"></typeAlias> </alias> <resultMaps> <resultMap id="Product-Result" class="Product"> <result property="ProductID" column="ProductID"/> <result property="Name" column="Name"/> <result property="Description" column="Description"/> <result property="Category" column="Category"/> <result property="Price" column="Price" nullValue="0.00" dbType="decimal"/> <result property="ImageURL" column="ImageURL" nullValue=""/> </resultMap> </resultMaps> <statements> <select id="Product-Select" parameterClass="Product" resultMap="Product-Result">select * from products </select> <insert id="Product-Insert" parameterClass="Product"> insert into products(Name,Description,Category,Price,ImageURL) values ( #Name#,#Description#,#Category#,#Price#,#ImageURL# ) <selectKey resultClass="int" property="ProductID" type="post">select @@IDENTITY </selectKey> </insert> <update id="Product-Update" parameterClass="Product"> update products set Name=#Name#,Description=#Description#,Category=#Category#,Price=#Price#,ImageURL=#ImageURL#where ProductID=#ProductID# </update> <delete id="Product-Delete" parameterClass="Product"> delete from products where ProductID=#ProductID# </delete> </statements> </sqlMap>
接着在AdminController里面重载一个处理HTTP Edit请求的action方法,如下:
[HttpPost]public ActionResult Edit(Product product, HttpPostedFileBase image)//HttpPostedFileBase提供了对上传的文件单独访问 {if (ModelState.IsValid)//如果验证通过才执行编辑 {if (image != null) {string path = @"D:\Study\Projects\SportsStore\SportsStore.WebUI\UploadImgs\" + image.FileName; image.SaveAs(path);//保存图片 product.ImageURL = image.FileName; } //接着执行保存的方法 repository.SaveProduct(product);TempData["message"] = string.Format("{0} has been saved", product.Name);return RedirectToAction("Index"); }else { //如果验证未通过,则继续显示编辑页面,重新编辑//there is something wrong with the data values return View(product); } }
当验证通过,会执行保存。这里我使用了TempData存储了一条提示信息,TempData是MVC框架提供的一个非常爽的功能。它是一个Dictionary的键值对,类似于Session和ViewBag。有一个关键的不同点是TempData会在HTTP请求结束被删除。
从上面的代码我们注意下Edit方法的返回值类型ActionResult.前面我使用大多是ViewResult。ViewResult是从ActionResult派生的,当我们想呈现一个View时就可以使用ViewResult的返回值类型。当然,ActionResult的其他类型也是可用的,其中一个就是RedirectToAction方法的返回值类型。
这里我们不能使用ViewBag,因为用户被重定向了,ViewBag在Controller和View传递数据并且ViewBag保存数据的时间不会比当前的Http请求,也就是说可能在Http请求没有结束时ViewBag里面数据已经丢失了,这就达不到我们要的效果。当然Session可以保存,但是Session会一直保存(除非服务器内存不足或是过期),直到我们显示的移除它,当然我们不情愿这样做。MVC框架提供的TempData功能就很完美的符合这里显示提示信息的要求。而且TempData里面的数据是限制在单个用户会话里面的(用户之前是不能相互查看对方的TempData),并且会一直保存知道我们读取了以后才被删除。我们将会在使用重定向到用户的action方法呈现View时读取TempData的数据。
接着我们在_AdminLayout里面使用TempData,为的是能够在引入了_AdminLayout的所以View里面使用。修改_AdminLayout.cshtml的代码如下:
View Code
<!DOCTYPE html><html><head> <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/Admin.css")" rel="Stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script></head><body> <div> @if (TempData["message"] != null) { <div class="Message">@TempData["message"]</div> } @RenderBody() </div></body></html>
上面我们引入了几个js文件是用来进行客户端验证的,这又是MVC框架提供给我们的一个非常爽的功能,我只需要在Product.cs里面为属性添加我需要限制的Attributes就能够实现验证用户输入的目的,引入了上面的两个js文件后,默认是开启了客户端验证的。当然服务器端验证是一定会有的。在大多数情况下,我们都希望进行客户端验证,因为这样会有一个比较好的用户体验。
但是如果我们不想进行客户端验证则可以这样做,则可以使用这两句代码:
HtmlHelper.ClientValidationEnabled = false;
HtmlHelper.UnobtrusiveJavaScriptEnabled = false;
如果我们将这两句代码放到一个View或Controller里面,那客户端验证对当前的Action就不会使用。
我们也可以如果在配置文件里面配置来禁止整个程序使用客户端验证,如下配置:
View Code
<configuration> <appSettings> <add key="ClientValidationEnabled" value="false"/> <add key="UnobtrusiveJavaScriptEnabled" value="false"/> </appSettings> </configuration>
接着我们实现创建一个新的Product,添加两个Create() action方法,一个是Get请求(没有加HttpGet Attribute表示默认Get),一个是处理HttpGet请求。我们呈现添加页面的时候调用的是Get,提交时调用Post。在AdminController里面添加的代码如下:
View Code
public ViewResult Create() {return View(new Product()); } [HttpPost]public ActionResult Create(Product product) {if (ModelState.IsValid) { repository.AddProduct(product); TempData["message"] = string.Format("{0} has been added", product.Name);return RedirectToAction("Index"); }else {return View(product); } }
接着右键添加Create.cshtml,代码如下:
View Code
@model SportsStore.Domain.Entities.Product@{ ViewBag.Title = "Create"; Layout = "~/Views/Shared/_AdminLayout.cshtml";}<h2> Create</h2>@using (Html.BeginForm()){ @Html.EditorForModel() <input type="submit" value="Add" />}
实现删除Product操作
首先添加一个接口方法:void DeleteProduct(Product product);
接着实现该方法
View Code
public class ProductsRepository : IProductsRepository { ISqlMapper mapper = MapperHelper.Instance();public IList<Product> Products {get { return mapper.QueryForList<Product>("Product-Select", ""); } }public void SaveProduct(Product product) { mapper.Update("Product-Update", product); } public void AddProduct(Product product) { mapper.Insert("Product-Insert", product); } public void DeleteProduct(Product product) { mapper.Delete("Product-Delete", product); } }
关于iBatisnet的映射文件,在上面已经全部列出来了,所以这里就不在展示了。接着是创建Delete action方法,如下:
View Code
[HttpPost]public ActionResult Delete(int productId) { Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId);if (prod != null) { repository.DeleteProduct(prod); TempData["message"] = string.Format("{0} was deleted", prod.Name); }return RedirectToAction("Index"); }
接着实现后台管理的安全性设置
asp.net mvc是建立在asp.net核心平台上的,所以我们可以访问asp.net form验证的功能,这也是一个比较通用的功能。只有登录成功的人才能操作后台。关于From验证,在书的22章会详细讲解。这里只是介绍了最基本的配置。在配置文件里面添加如下代码:
View Code
<authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880"> <credentials passwordFormat="Clear"> <user name="admin" password="secret" /> </credentials> </forms> </authentication> <!--这里的timeout是2880分钟即48小时,这里我们硬编码用户名和密码,实际情况肯定不是这样。-->
MVC框架有一个非常强大的功能称为filters。这些都是Attributes,应用到一个action方法或Controller类。当一个请求被处理的时候,这些Attributes引入了额外的逻辑。我们也可以定义自己的filter。
这部分内容会在13章详细介绍的,这里大概知道就行。其实我理解这里就是,filter功能用来实现权限控制,哪些action是需要权限的,只允许哪些用户方法的等等。当添加了这些filter后,处理请求的时候就会做额外的判断。
我们这里只是用到了Authorize。我们在AdminController类的上面添加这个filter,如下所示:
View Code
using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System.Linq; namespace SportsStore.WebUI.Controllers { [Authorize] public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } ...
这样就表示只有通过验证的才可能访问该Controller,当然我可以细化到针对一个action方法。
这个时候运行程序,通过/Admin/Index URL进行后台会报错,因为并不存在Account/LogOn.
下面我们开始实现登录验证功能,在Infrastructure文件夹里面再创建一个Abstract文件夹,在该文件夹里面添加一个接口IAuthProvider,代码如下:
View Code
namespace SportsStore.WebUI.Infrastructure.Abstract { public interface IAuthProvider { bool Authenticate(string username, string password); } }
接着在Infrastructure文件夹里面再创建一个Concrete文件夹,添加一个实现类FormsAuthProvider.如下:
View Code
using System.Web.Security; using SportsStore.WebUI.Infrastructure.Abstract; namespace SportsStore.WebUI.Infrastructure.Concrete { public class FormsAuthProvider : IAuthProvider { public bool Authenticate(string username, string password) { bool result = FormsAuthentication.Authenticate(username, password);if (result) { FormsAuthentication.SetAuthCookie(username, false); } return result; } } }
然后使用Ninject添加绑定,如下:
private void AddBindings() { ninjectKernel.Bind<IProductsRepository>().To<ProductsRepository>(); EmailSettings emailSettings = new EmailSettings { WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false") }; ninjectKernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings); ninjectKernel.Bind<IAuthProvider>().To<FormsAuthProvider>(); }
接着创建一个AccountController和LogOn action方法,实际上是重载的两个LogOn方法,一个处理Get呈现,一个处理Post提交验证。这之前我们需要创建一个view model来传递。在SportsStore.WebUI/Models里面添加一个新的类LogOnViewModel,代码如下:
View Code
using System.ComponentModel.DataAnnotations; namespace SportsStore.WebUI.Models { public class LogOnViewModel { [Required] public string UserName { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } } }
接着我们添加AccountController,如下:
View Code
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Mvc;using SportsStore.WebUI.Infrastructure.Abstract;using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers{public class AccountController : Controller {private IAuthProvider authProvider;public AccountController(IAuthProvider auth) { authProvider = auth; }public ViewResult LogOn() {return View(); } [HttpPost]public ActionResult LogOn(LogOnViewModel model, string returnUrl) {if (ModelState.IsValid) {if (authProvider.Authentication(model.UserName, model.Password)) {return Redirect(returnUrl ?? Url.Action("Index", "Admin")); }else { ModelState.AddModelError("", "Incorrect username or password");return View(); } }else {return View(); } } }}
右键添加LogOn View,如下:
View Code
@model SportsStore.WebUI.Models.LogOnViewModel @{ ViewBag.Title = "LogOn"; Layout = "~/Views/Shared/_AdminLayout.cshtml";} <h2>Log In</h2><p>Please log in to access the administrator area:</p>@using (Html.BeginForm()){ @Html.ValidationSummary(true) @Html.EditorForModel() <p><input type="submit" value="Log in" /></p> }
到这里,整个项目就结束了,呵呵!通过这个项目对MVC的理解应该加深了,后面进入到第二部分MVC3框架的讲解。因为项目后面对数据库进行了更改,所以你需要自己在相关的View里面添加显示图片的代码。如果你是按照我的笔记实际操作,遇到任何问题请及时留言!
好了,今天的笔记就到这里,祝大家周末愉快!公司如果这几天有年会的,祝大家都有好运中奖!