一步步打造一个简单的 MVC 电商网站 - BooksStore(二)
本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore
《一步步打造一个简单的 MVC 电商网站 - BooksStore(一)》
《一步步打造一个简单的 MVC 电商网站 - BooksStore(二)》
《一步步打造一个简单的 MVC 电商网站 - BooksStore(三)》
《一步步打造一个简单的 MVC 电商网站 - BooksStore(四)》
简介
上一次我们尝试了:创建项目架构、创建域模型实体、创建单元测试、创建控制器与视图、创建分页和加入样式,而这一节我们会完成两个功能,分类导航与购物车。
主要功能与知识点如下:
分类、产品浏览、购物车、结算、CRUD(增删改查) 管理、发邮件、分页、模型绑定、认证过滤器和单元测试等(预计剩余两篇,预计明天(因为周六不放假)和周三(因为周二不上班)发布)。
【备注】项目使用 VS2015 + C#6 进行开发,有问题请发表在留言区哦,还有,页面长得比较丑,请见谅。
目录
添加分类导航
加入购物车
创建一个分部视图 Partial View
一、添加分类导航
上一次我们把网页划分成了三个模块,其中左侧栏的部分尚未完成,左侧栏拥有将书籍分类展示的功能。
图 1
1.回到之前的BookDetailsViewModels 视图模型,我们额外再添加一个新的属性用作分类(CurrentCategory):
////// 书籍详情视图模型 /// public class BookDetailsViewModels : PagingInfo { public IEnumerableBooks { get; set; } /// /// 当前分类 /// public string CurrentCategory { get; set; } }
2.修改完视图模型,现在就应该修改对应的 BookController 中的Details 方法
////// 详情 /// /// 分类 /// 页码 ///public ActionResult Details(string category, int pageIndex = 1) { var model = new BookDetailsViewModels { Books = _bookRepository.Books.Where(x => category == null || x.Category == category) .OrderBy(x => x.Id) .Skip((pageIndex - 1) * PageSize) .Take(PageSize), CurrentCategory = category, PageSize = PageSize, PageIndex = pageIndex, TotalItems = _bookRepository.Books.Count(x => category == null || x.Category == category) }; return View(model); }
BookController.cs
namespace Wen.BooksStore.WebUI.Controllers
{
public class BookController : Controller
{
private readonly IBookRepository _bookRepository;
public int PageSize = 5;
public BookController(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
///
/// 详情
///
/// 分类
/// 页码
///
public ActionResult Details(string category, int pageIndex = 1)
{
var model = new BookDetailsViewModels
{
Books =
_bookRepository.Books.Where(x => category == null || x.Category == category)
.OrderBy(x => x.Id)
.Skip((pageIndex - 1) * PageSize)
.Take(PageSize),
CurrentCategory = category,
PageSize = PageSize,
PageIndex = pageIndex,
TotalItems = _bookRepository.Books.Count(x => category == null || x.Category == category)
};
return View(model);
}
}
}
参数增加了一个 category,用于获取分类的字符串,对应 Books 中的属性的赋值语句改为_bookRepository.Books.Where(x => category == null || x.Category == category),这里的 Lambda 表达式x => category == null || x.Category ==category 的意思是,分类字符串为空就取库中所有的 Book 实体,不为空时根据分类进行对集合进行筛选过滤。
还要对属性 CurrentCategory 进行赋值。
别忘了,因为分页是根据 TotalItems 属性进行的,所以还要修改地方_bookRepository.Books.Count(x => category == null || x.Category == category),通过 LINQ 统计不同分类情况的个数。
3.该控制器对应的 Details.cshtml 中的分页辅助器也需要修改,添加新的路由参数:
@Html.Pagelinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory }))
Details.cshtml
@model Wen.BooksStore.WebUI.Models.BookDetailsViewModels
@{
ViewBag.Title = "Books";
}
@foreach (var item in Model.Books)
{
@item.Name
@item.Description
@item.Price.ToString("C")
}
@Html.Pagelinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory }))
4.路由区域也应当修改一下
RouteConfig.cs
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}",
defaults: new { controller = "Book", action = "Details" }
);
routes.MapRoute(
name: null,
url: "{controller}/{action}/{category}",
defaults: new { controller = "Book", action = "Details" }
);
routes.MapRoute(
name: null,
url: "{controller}/{action}/{category}/{pageIndex}",
defaults: new { controller = "Book", action = "Details", pageIndex = UrlParameter.Optional }
);
}
5.现在新建一个名为 NavController 的控制器,并添加一个名为Sidebar 的方法,专门用于渲染左侧边栏。
不过返回的 View 视图类型变成 PartialView 分部视图类型:
public PartialViewResult Sidebar(string category = null)
{
var categories = _bookRepository.Books.Select(x => x.Category).Distinct().OrderBy(x => x);
return PartialView(categories);
}
在方法体在右键,添加一个视图,勾上创建分部视图。
Sidebar.cshtml 修改为:
@model IEnumerable
- @Html.Actionlink("所有分类", "Details", "Book")
@foreach (var item in Model) {- @Html.Routelink(item, new { controller = "Book", action = "Details", category = item, pageIndex = 1 }, new { @class = item == ViewBag.CurrentCategory ? "selected" : null })
}
MVC 框架具有一种叫作“子动作(Child Action)”的概念,可以适用于重用导航控件之类的东西,使用类似 RenderAction() 的方法,在当前的视图中输出指定的动作方法。
因为需要在父视图中呈现另一个 Action 中的分部视图,所以原来的_Layout.cshtml布局页修改如下:
现在,启动的结果应该和图 1 是一样的,尝试点击左侧边栏的分类,观察主区域的变化情况。
二、加入购物车
图 2
界面的大体功能如图 2,在每本图书的区域新增一个链接(添加到购物车),会跳转到一个新的页面,显示购物车的详细信息 - 购物清单,也可以通过“结算”链接跳转到一个新的页面。
购物车是应用程序业务域的一部分,因此,购物车实体应该为域模型。
1.添加两个类:
Cart.cs 有添加、移除、清空和统计功能:
////// 购物车 /// public class Cart { private readonly List_cartItems = new List (); /// /// 获取购物车的所有项目 /// public IListGetCartItems => _cartItems; /// /// 添加书模型 /// /// /// public void AddBook(Book book, int quantity) { if (_cartItems.Count == 0) { _cartItems.Add(new CartItem() { Book = book, Quantity = quantity }); return; } var model = _cartItems.FirstOrDefault(x => x.Book.Id == book.Id); if (model == null) { _cartItems.Add(new CartItem() { Book = book, Quantity = quantity }); return; } model.Quantity += quantity; } ////// 移除书模型 /// /// public void RemoveBook(Book book) { var model = _cartItems.FirstOrDefault(x => x.Book.Id == book.Id); if (model == null) { return; } _cartItems.RemoveAll(x => x.Book.Id == book.Id); } ////// 清空购物车 /// public void Clear() { _cartItems.Clear(); } ////// 统计总额 /// ///public decimal ComputeTotalValue() { return _cartItems.Sum(x => x.Book.Price * x.Quantity); } }
CartItem.cs 表示购物车中的每一项:
////// 购物车项 /// public class CartItem { ////// 书 /// public Book Book { get; set; } ////// 数量 /// public int Quantity { get; set; } }
2.修改一下之前的 Details.cshtml,增加“添加到购物车”的按钮:
@model Wen.BooksStore.WebUI.Models.BookDetailsViewModels
@{
ViewBag.Title = "Books";
}
@foreach (var item in Model.Books)
{
@item.Name
@item.Description
@item.Price.ToString("C")
@using (Html.BeginForm("AddToCart", "Cart"))
{
var id = item.Id;
@Html.HiddenFor(x => id);
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
}
}
@Html.Pagelinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory }))
【备注】@Html.BeginForm() 方法默认会创建一个 Post 请求方法的表单,为什么不直接使用 Get 请求呢,HTTP 规范要求,会引起数据变化时不要使用 Get 请求,将产品添加到一个购物车明显会出现新的数据变化,所以,这种情形不应该使用 Get 请求,直接显示页面或者列表数据,这种请求才应该使用 Get。
3.先修改下 css 中的样式
body {
}
#header, #content, #sideBar {
display: block;
}
#header {
background-color: green;
border-bottom: 2px solid #111;
color: White;
}
#header, .title {
font-size: 1.5em;
padding: .5em;
}
#sideBar {
float: left;
width: 8em;
padding: .3em;
}
#content {
border-left: 2px solid gray;
margin-left: 10em;
padding: 1em;
}
.pager {
text-align: right;
padding: .5em 0 0 0;
margin-top: 1em;
}
.pager A {
font-size: 1.1em;
color: #666;
padding: 0 .4em 0 .4em;
}
.pager A:hover {
background-color: Silver;
}
.pager A.selected {
background-color: #353535;
color: White;
}
.item input {
float: right;
color: White;
background-color: green;
}
.table {
width: 100%;
padding: 0;
margin: 0;
}
.table th {
font: bold 12px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
color: #4f6b72;
border-right: 1px solid #C1DAD7;
border-bottom: 1px solid #C1DAD7;
border-top: 1px solid #C1DAD7;
letter-spacing: 2px;
text-transform: uppercase;
text-align: left;
padding: 6px 6px 6px 12px;
background: #CAE8EA no-repeat;
}
.table td {
border-right: 1px solid #C1DAD7;
border-bottom: 1px solid #C1DAD7;
background: #fff;
font-size: 14px;
padding: 6px 6px 6px 12px;
color: #4f6b72;
}
.table td.alt {
background: #F5FAFA;
color: #797268;
}
.table th.spec, td.spec {
border-left: 1px solid #C1DAD7;
}
4.再添加一个 CartController
////// 购物车 /// public class CartController : Controller { private readonly IBookRepository _bookRepository; public CartController(IBookRepository bookRepository) { _bookRepository = bookRepository; } ////// 首页 /// /// ///public ViewResult Index(string returnUrl) { return View(new CartIndexViewModel() { Cart = GetCart(), ReturnUrl = returnUrl }); } /// /// 添加到购物车 /// /// /// ///public RedirectToRouteResult AddToCart(int id, string returnUrl) { var book = _bookRepository.Books.FirstOrDefault(x => x.Id == id); if (book != null) { GetCart().AddBook(book, 1); } return RedirectToAction("Index", new { returnUrl }); } /// /// 从购物车移除 /// /// /// ///public RedirectToRouteResult RemoveFromCart(int id, string returnUrl) { var book = _bookRepository.Books.FirstOrDefault(x => x.Id == id); if (book != null) { GetCart().RemoveBook(book); } return RedirectToAction("Index", new { returnUrl }); } /// /// 获取购物车 /// ///private Cart GetCart() { var cart = (Cart)Session["Cart"]; if (cart != null) return cart; cart = new Cart(); Session["Cart"] = cart; return cart; } }
【备注】这里的购物车是通过 Session 会话状态进行保存用户的 Cart 对象。当会话过期(典型的情况是用户很长时间没有对服务器发起任何请求),与该会话关联的数据就会被删除,这就意味着不需要对 Cart 对象进行生命周期的管理。
【备注】RedirectToAction() 方法:将一个 HTTP 重定向的指令发给客户端浏览器,要求浏览器请求一个新的 Url。
5.在 Index 方法中选择右键新建视图,专门用于显示购物清单:
Index.cshtml 中的代码:
@model Wen.BooksStore.WebUI.Models.CartIndexViewModel 我的购物车
| 书名 | 价格 | 数量 | 总计 |
|---|---|---|---|
| @item.Book.Name | @item.Book.Price | @item.Quantity | @((item.Book.Price * item.Quantity).ToString("C")) |
| 总计: | @Model.Cart.ComputeTotalValue().ToString("C") |
继续购物
我想,这一定是一个令人激动的时刻,因为我们已经完成了这个基本的添加到购物车的功能。
三、创建一个分部视图 Partial View
分部视图,是嵌入在另一个视图中的一个内容片段,并且可以跨视图重用,这有助于减少重复,尤其需要在多个地方需要重复使用相同的数据时。
在 Shared 内部新建一个名为_BookSummary.cshtml 的视图,并且把之前Details.cshtml 的代码进行整理。
修改后的两个视图:
Details.cshtml
@model Wen.BooksStore.WebUI.Models.BookDetailsViewModels
@{
ViewBag.Title = "Books";
}
@foreach (var item in Model.Books)
{
Html.RenderPartial("_BookSummary", item);
}
@Html.Pagelinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory }))
_BookSummary.cshtml
@model Wen.BooksStore.Domain.Entities.Book
@Model.Name
@Model.Description
@Model.Price.ToString("C")
@using (Html.BeginForm("AddToCart", "Cart"))
{
var id = Model.Id;
@Html.HiddenFor(x => id);
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持考高分网。



