本文将会创建一个基本的电子商务网站。由于电子商务网站的基本功能都是差不多的,此处省去了需求分析等工作,直接总结出结论。分为4个基本功能:
其中每项还可以细分,由于比较简单不再赘述。
由于项目是非常简单的,这里并没有采用分层结构。MVC分别代表了三个部分,Model、View和Control。开发过程中创立对象时谁先谁后?在《ASP.NET MVC4开发指南》中(本文就是采用了该书中的实例),作者黄保翕这样阐述“以笔者的实务开发经验来说,觉得M(Model)是MVC架构的中心,有了Model之后就可以让Controller与View参考这些Model(模型),先定义出计划开发的Controller与Action,然后再创建所有Action对应的View(无属性的View),之后就可以将不同单元的Controller与View分工开发,最后再进行集成即可”,本项目的实现步骤就基本是按照这样的思想。首先按照功能分为4个部分(这样进一步简化了问题),每个功能的实现通过三步来完成,最后把他们集成到一起。
另外,虽然原来的范例中使用了code first技术,我等菜鸟还是习惯从db first,对localdb不甚了解,这个范例还是先建了数据库MVCShopping,并录入了一些测试数据。
依次实现各个部分:
这是最简单通用的一部分,与经典的MVC Movie Store中所呈现的业务逻辑一样。包括:商品类别列表、某类别下的商品列表和某一商品的详细信息。按照实现步骤,分三步实现:
数据模型涉及到了两个数据实体,商品类别(ProductCategory)和商品信息(Product)。在Models文件夹下新建两个类:ProductCategory和Product
[DisplayName("商品类别")] [DisplayColumn("Name")] public class ProductCategory { [Key] public int Id { get; set; } [DisplayName("商品类别名称")] [Required(ErrorMessage="请输入商品类别名称")] [MaxLength(20,ErrorMessage="类别名称不可超过20个字")] public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } }
[DisplayName("商品信息")] [DisplayColumn("Name")] public class Product { [Key] public int Id { get; set; } [DisplayName("商品类别")] [Required] public virtual ProductCategory ProductCategory { get; set; } [DisplayName("商品名称")] [Required(ErrorMessage = "请输入商品名称")] [MaxLength(60, ErrorMessage = "不得超过60个字")] public string Name { get; set; } [DisplayName("商品简介")] [Required(ErrorMessage = "请输入商品简介")] [MaxLength(250, ErrorMessage = "商品简介不超过250字")] public string Description { get; set; } [DisplayName("商品颜色")] [Required(ErrorMessage = "请选择颜色")] public Color Color { get; set; } [DisplayName("商品售价")] [Required(ErrorMessage = "请输入商品售价")] [Range(1,10000,ErrorMessage="商品售价必须介于1到10000之间")] public int Price { get; set; } [DisplayName("上架时间")] [Description("如果不设置上架时间,代表不发售")] public DateTime? PublishOn{get;set;} }
在这里可能新手会比较困惑的是每个字段上都有形如[DisplayName("商品名称")] [Required]等内容。这些统称为注解,可分为验证注解和显示注解两类。后面会见到很多诸如此类的注解,便于方便和验证。
浏览商品主要会涉及到三个页面:“首页”显示商品类别列表,“列表页”显示商品列表,“详细页”显示商品明细。把这三个也的功能放在一个Controller控制器中。在Controllers文件夹中添加控制器,HomeController。
public class HomeController : Controller { MvcShoppingContext db = new MvcShoppingContext(); // 首页 public ActionResult Index() { var data = db.ProductCategories.ToList(); data = db.ProductCategories.ToList(); return View(data); } //商品列表 public ActionResult ProductList(int id) { var productCategory = db.ProductCategories.Find(id); var data = productCategory.Products.ToList(); return View(data); } //商品明细 public ActionResult ProductDetail(int id) { var data = db.Products.Find(id); return View(data); } }
最后创建展示页面。在HomeController控制器中添加相应的视图。
Index.cshtml
@model IEnumerable<MVCShopping.Models.ProductCategory> <h2>@Html.DisplayNameFor(t=>t.Name)</h2> <ul> @foreach (var item in Model) { <li>@Html.ActionLink(item.Name, "ProductList", new { id=item.Id})</li> } </ul>
@model IEnumerable<MVCShopping.Models.Product> @{ var ajaxOption = new AjaxOptions() { HttpMethod="Post", OnSuccess = "AddToCartSuccess", OnFailure = "AddToCartFailure", }; } @section scripts{ @Scripts.Render("~/bundles/jqueryval") <script> function AddToCartSuccess() { alert('添加购物从成功'); } function AddToCartFailure(xhr) { alert('添加购物车失败(HTTP状态代码:'+xhr.status+')'); } </script> } <h2>@Html.DisplayNameFor(t => t.ToList()[0])</h2> <h2>@Html.DisplayNameFor(t => t.Name)</h2> <h3>您正在浏览[@Model.First().ProductCategory.Name]分类的信息</h3> <table> <tr> <th>@Html.DisplayNameFor(model=>model.Name)</th> <th>@Html.DisplayNameFor(model => model.Description)</th> <th>@Html.DisplayNameFor(model => model.Price)</th> <th>添加购物车</th> </tr> @foreach (var item in Model) { <tr> <td>@Html.ActionLink(item.Name, "ProductDetail", new { id=item.Id})</td> <td>@Html.DisplayFor(w=>item.Description)</td> <td>@Html.DisplayFor(w=>item.Price)</td> <td>@Ajax.ActionLink("添加购物车", "AddToCart", "Cart", new { ProductId=item.Id},ajaxOption)</td> </tr> } </table>
@model MVCShopping.Models.Product @{ var ajaxOption = new AjaxOptions() { OnSuccess = "AddToCartSuccess", OnFailure = "AddToCartFailure", }; } @section scripts{ @Scripts.Render("~/bundles/jqueryval") <script> function AddToCartSuccess() { alert('添加成功'); } function AddToCartFailure(xhr) { alert('添加购物车失败(HTTP状态代码:' + xhr.status + ')'); } </script> } <h2>您正在察看"@Model.Name"商品</h2> <fieldset> <legend>@Html.DisplayNameFor(m=>m)</legend> <div class="display-label"> @Html.DisplayNameFor(model=>model.Description) </div> <div class="display-field"> @Html.DisplayFor(model => model.Description) </div> <div class="display-label"> @Html.DisplayNameFor(model => model.Price) </div> <div class="display-field"> @Html.DisplayFor(model => model.Price) </div> <div class="display-label"> @Html.DisplayNameFor(model => model.PublishOn) </div> <div class="display-field"> @Html.DisplayFor(model => model.PublishOn) </div> </fieldset> <p> @Ajax.ActionLink("添加购物车","AddToCart","Cart",ajaxOption) </p>
1)数据模型规划
Member.cs
[DisplayName("会员信息")] [DisplayColumn("Name")] public class Member { [Key] public int id { get; set; } [DisplayName("会员帐号")] [Description("以Email为会员的登陆帐号")] [Required(ErrorMessage = "请输入Email地址")] [MaxLength(250, ErrorMessage = "不得超过250个字")] [DataType(DataType.EmailAddress)] public string Email { get; set; } [DisplayName("会员密码")] [Description("密码将以SHA1进行哈西运算,通过运算后的结果转为HEX表示法的字符串长度都为40")] [Required(ErrorMessage = "请输入密码")] [MaxLength(40, ErrorMessage = "不得超过40个字")] [DataType(DataType.Password)] public string Password { get; set; } [DisplayName("中文姓名")] [Description("忽略外国人")] [Required(ErrorMessage = "请输入中文姓名")] [MaxLength(5, ErrorMessage = "不得超过5个字")] public string Name { get; set; } [DisplayName("网络昵称")] [Required(ErrorMessage = "请输入网络昵称")] [MaxLength(15, ErrorMessage = "不得超过15个字")] public string Nickname { get; set; } [DisplayName("会员注册时间")] public DateTime RegisterOn { get; set; } //AuthCode会保存一个GUID值 [DisplayName("会员启用认证码")] [MaxLength(36)] [Description("当AuthCode等于null代表会员已通过Email认证")] public string AuthCode { get; set; } public virtual ICollection<OrderHeader> orders { get; set; } }
其中的字段AuthCode是为了可以进行会员验证功能的。当你注册一个账号时常常能收到一封确认邮件,这个字段就是为了实现此功能的。在后面的功能扩展中会进行展示。
MemberLoginViewModel.cs
public class MemberLoginViewModel { /// <summary> /// 在帐户这显示指定了DataType(DataType.EmailAddress,ErrorMessage="请输入您的Email地址") /// 但是并不现实错误信息,这是因为MVC4并没有针对DataType属性支持客户端的js验证功能 /// </summary> [DisplayName("会员帐号")] [Required(ErrorMessage = "请输入{0}")] [DataType(DataType.EmailAddress,ErrorMessage="请输入您的Email地址")] public string Email { get; set; } [DisplayName("会员密码")] [Required(ErrorMessage = "请输入{0}")] [DataType(DataType.Password)] public string Password { get; set; } }这个Model是为登录界面提供的。由于登录时只有两个可以用到的字段,为了能够使用强类型方式使用Model,所有新建了一个ViewModel。
2)控制器架构规划
public class MemberController : Controller { MvcShoppingContext db = new MvcShoppingContext(); private string pwSalt = "AlrySq1oPe2Mh784QQwG6jRAfkdPpDa90J0i"; // 会员注册页面 public ActionResult Register() { return View(); } //写入会员信息 [HttpPost] public ActionResult Register([Bind(Exclude="RegisterOn,AuthCode")]Member member) { //检查会员是否存在 var chk_member = db.Members.Where(p => p.Email == member.Email).FirstOrDefault(); if (chk_member != null) { ModelState.AddModelError("Email","您输入的Email已经有人注册过了!"); } if (ModelState.IsValid) { //将密码加盐在之后进行哈希运算 member.Password = FormsAuthentication.HashPasswordForStoringInConfigFile(pwSalt + member.Password, "SHA1"); member.RegisterOn = DateTime.Now; //会员验证码,采用Guid当成验证码属性,避免有会员使用到重复的验证码 member.AuthCode = Guid.NewGuid().ToString(); db.Members.Add(member); db.SaveChanges(); return RedirectToAction("Index","Home"); } else { return View(); } } //显示会员登陆页面 public ActionResult Login(string returnUrl) { ViewBag.ReturnUrl = returnUrl; return View(); } //会员登陆 [HttpPost] public ActionResult Login(string email, string password, string returnUrl) { if (ValidateUser(email, password)) { FormsAuthentication.SetAuthCookie(email,false); if (string.IsNullOrEmpty(returnUrl)) { return RedirectToAction("Index", "Home"); } else return Redirect(returnUrl); } ModelState.AddModelError("", "输入的帐号或者密码错误"); return View(); } private bool ValidateUser(string email, string password) { var hash_pw = FormsAuthentication.HashPasswordForStoringInConfigFile(pwSalt + password, "SHA1"); var member = db.Members.Where(p => p.Email == email && p.Password == hash_pw).FirstOrDefault(); return(member!=null); } //会员注销 public ActionResult Logout() { FormsAuthentication.SignOut(); Session.Clear(); return RedirectToAction("Index", "Home"); } public ActionResult ValidateRegister() { return View(); } }
带有[HttpPost]标记的表明是Post请求时执行的部分。默认是[HttpGet]。会员功能主要就是包含了注册和登录两部分。
3)创建视图页面
Login.cshtml
@model MVCShopping.Models.MemberLoginViewModel @{ ViewBag.Title = "Login"; } <h2>Login</h2> @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>MemberLoginViewModel</legend> <div class="editor-label"> @Html.LabelFor(model => model.Email) </div> <div class="editor-field"> @Html.TextBoxFor(model => model.Email, new { data_val_Email="请输入Email地址"}) @Html.ValidationMessageFor(model => model.Email) </div> <div class="editor-label"> @Html.LabelFor(model => model.Password) </div> <div class="editor-field"> @Html.EditorFor(model => model.Password) @Html.ValidationMessageFor(model => model.Password) </div> <p> <input type="submit" value="登陆" /> </p> </fieldset> } <div> @Html.ActionLink("Back to List", "Index") </div> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
@model MVCShopping.Models.Member @{ ViewBag.Title = "注册"; } <h2>会员注册</h2> @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>请输入会员注册信息</legend> <div class="editor-label"> @Html.LabelFor(model => model.Email) </div> <div class="editor-field"> @Html.EditorFor(model => model.Email) @Html.ValidationMessageFor(model => model.Email) </div> <div class="editor-label"> @Html.LabelFor(model => model.Password) </div> <div class="editor-field"> @Html.EditorFor(model => model.Password) @Html.ValidationMessageFor(model => model.Password) </div> <div class="editor-label"> @Html.LabelFor(model => model.Name) </div> <div class="editor-field"> @Html.EditorFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name) </div> <div class="editor-label"> @Html.LabelFor(model => model.Nickname) </div> <div class="editor-field"> @Html.EditorFor(model => model.Nickname) @Html.ValidationMessageFor(model => model.Nickname) </div> <p> <input type="submit" value="注册" /> </p> </fieldset> } <div> @Html.ActionLink("Back to List", "index") </div> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
List<Cart> Carts
{
get {
if (Session["Carts"] == null)
{
Session["Carts"] = new List<Cart>();
}
return Session["Carts"] as List<Cart>;
}
set { Session["Carts"] = value; }
}
1)数据模型规划
public class Cart { [DisplayName("选购商品")] [Required] public Product Product { get; set; } [DisplayName("选购数量~")] [Required] public int Amount { get; set; } }
2)控制器架构规划
public class CartController : Controller { //非会员也可使用所以购物车保存在Session中 // 显示当前购物从项目 MvcShoppingContext db = new MvcShoppingContext(); List<Cart> Carts { get { if (Session["Carts"] == null) { Session["Carts"] = new List<Cart>(); } return Session["Carts"] as List<Cart>; } set { Session["Carts"] = value; } } public ActionResult Index() { return View(this.Carts); } //添加产品项目到购物车,如果没有传入Amount参数则默认购买数量为1 //因为要通过Ajax调用这个Action,所以可以先标示Post属性 [HttpPost] public ActionResult AddToCart(int ProductId, int Amount = 1) { var product = db.Products.Find(ProductId); //验证产品是否存在 if (product == null) return HttpNotFound(); var existiongCart = this.Carts.FirstOrDefault(p => p.Product.Id == ProductId); if (existiongCart != null) { existiongCart.Amount += 1; } else { this.Carts.Add(new Cart() { Product=product,Amount=Amount}); } return new HttpStatusCodeResult(HttpStatusCode.Created); } //移出购物从项目 [HttpPost] public ActionResult Remove(int ProductId) { var existingCart = this.Carts.FirstOrDefault(p => p.Product.Id == ProductId); if (existingCart != null) { this.Carts.Remove(existingCart); } return new HttpStatusCodeResult(System.Net.HttpStatusCode.OK); } //更新数量 [HttpPost] public ActionResult UpdateAmount(List<Cart> Carts) { foreach (var item in Carts) { var existingCart = this.Carts.FirstOrDefault(p => p.Product.Id == item.Product.Id); if (existingCart != null) { existingCart.Amount = item.Amount; } } return RedirectToAction("Index","Cart"); } }
3)创建视图页面
index.cshtml
@model IEnumerable<MVCShopping.Models.Cart> @{ var ajaxOption = new AjaxOptions() { OnSuccess = "RemoveCartSuccess", OnFailure = "RemoveCartFailure", Confirm = "您确定要从购物车删除吗?", HttpMethod = "Post", }; } @section scripts{ @Scripts.Render("~/bundles/jqueryval") <script> function RemoveCartSuccess() { alert('移出购物从成功'); location.reload(); } function RemoveCartFailure(xhr) { alert('移出购物车失败(Http状态代吗:' + xhr.status); } </script> } <h2>购物车列表</h2> @using (Html.BeginForm("UpdateAmount", "Cart")) { <table> <tr> <th>产品名称</th> <th>单价</th> <th>数量</th> <th>小计</th> <th></th> </tr> @{int subTotal = 0;} @foreach (var item in Model) { subTotal += item.Product.Price * item.Amount; var ddlAmountList = new SelectList(Enumerable.Range(1,10),item.Amount); @Html.Hidden(item.Product.Id.ToString()) <tr> <td>@Html.DisplayFor(t=>item.Product.Name)</td> <td>NT¥@(item.Product.Price)</td> <td>@Html.DropDownListFor(t=>item.Amount,ddlAmountList)</td> <td>NT¥@(item.Product.Price*item.Amount)</td> <td> @Ajax.ActionLink("删除","Remove",new{ProductId=item.Product.Id},ajaxOption) </td> </tr> } <tr> <th></th> <th></th> <th>总价</th> <th id="subtotal">NT¥ @subTotal</th> <th></th> </tr> </table> <p> <input type="submit" value="更新数量" /> <input type="button" value="完成订单" onclick="location.href='@Url.Action("Complete","Order")';" /> </p> }