一步步打造一個簡單的 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):
/// <summary> /// 書籍詳情視圖模型 /// </summary> public class BookDetailsViewModels : PagingInfo { public IEnumerable<Book> Books { get; set; } /// <summary> /// 當前分類 /// </summary> public string CurrentCategory { get; set; } }
2.修改完視圖模型,現在就應該修改對應的 BookController 中的 Details 方法
/// <summary> /// 詳情 /// </summary> /// <param name="category">分類</param> /// <param name="pageIndex">頁碼</param> /// <returns></returns> 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); }

namespace Wen.BooksStore.WebUI.Controllers { public class BookController : Controller { private readonly IBookRepository _bookRepository; public int PageSize = 5; public BookController(IBookRepository bookRepository) { _bookRepository = bookRepository; } /// <summary> /// 詳情 /// </summary> /// <param name="category">分類</param> /// <param name="pageIndex">頁碼</param> /// <returns></returns> 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 中的分頁輔助器也需要修改,添加新的路由參數:
<div class="pager"> @Html.PageLinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory })) </div>

1 @model Wen.BooksStore.WebUI.Models.BookDetailsViewModels 2 3 @{ 4 ViewBag.Title = "Books"; 5 } 6 7 @foreach (var item in Model.Books) 8 { 9 <div class="item"> 10 <h3>@item.Name</h3> 11 @item.Description 12 <h4>@item.Price.ToString("C")</h4> 13 <br /> 14 <hr /> 15 </div> 16 } 17 18 <div class="pager"> 19 @Html.PageLinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory })) 20 </div>
4.路由區域也應當修改一下

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<string> <ul> <li>@Html.ActionLink("所有分類", "Details", "Book")</li> @foreach (var item in Model) { <li>@Html.RouteLink(item, new { controller = "Book", action = "Details", category = item, pageIndex = 1 }, new { @class = item == ViewBag.CurrentCategory ? "selected" : null })</li> } </ul>
MVC 框架具有一種叫作“子動作(Child Action)”的概念,可以適用于重用導航控件之類的東西,使用類似 RenderAction() 的方法,在當前的視圖中輸出指定的動作方法。
因為需要在父視圖中呈現另一個 Action 中的分部視圖,所以原來的 _Layout.cshtml 布局頁修改如下:
現在,啟動的結果應該和圖 1 是一樣的,嘗試點擊左側邊欄的分類,觀察主區域的變化情況。
二、加入購物車
圖 2
界面的大體功能如圖 2,在每本圖書的區域新增一個鏈接(添加到購物車),會跳轉到一個新的頁面,顯示購物車的詳細信息 - 購物清單,也可以通過“結算”鏈接跳轉到一個新的頁面。
購物車是應用程序業務域的一部分,因此,購物車實體應該為域模型。
1.添加兩個類:
Cart.cs 有添加、移除、清空和統計功能:
/// <summary> /// 購物車 /// </summary> public class Cart { private readonly List<CartItem> _cartItems = new List<CartItem>(); /// <summary> /// 獲取購物車的所有項目 /// </summary> public IList<CartItem> GetCartItems => _cartItems; /// <summary> /// 添加書模型 /// </summary> /// <param name="book"></param> /// <param name="quantity"></param> 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; } /// <summary> /// 移除書模型 /// </summary> /// <param name="book"></param> 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); } /// <summary> /// 清空購物車 /// </summary> public void Clear() { _cartItems.Clear(); } /// <summary> /// 統計總額 /// </summary> /// <returns></returns> public decimal ComputeTotalValue() { return _cartItems.Sum(x => x.Book.Price * x.Quantity); } }
CartItem.cs 表示購物車中的每一項:
/// <summary> /// 購物車項 /// </summary> public class CartItem { /// <summary> /// 書 /// </summary> public Book Book { get; set; } /// <summary> /// 數量 /// </summary> public int Quantity { get; set; } }
2.修改一下之前的 Details.cshtml,增加“添加到購物車”的按鈕:
@model Wen.BooksStore.WebUI.Models.BookDetailsViewModels @{ ViewBag.Title = "Books"; } @foreach (var item in Model.Books) { <div class="item"> <h3>@item.Name</h3> @item.Description <h4>@item.Price.ToString("C")</h4> @using (Html.BeginForm("AddToCart", "Cart")) { var id = item.Id; @Html.HiddenFor(x => id); @Html.Hidden("returnUrl", Request.Url.PathAndQuery) <input type="submit" value="+ 添加到購物車" /> } <br /> <hr /> </div> } <div class="pager"> @Html.PageLinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory })) </div>
【備注】@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
/// <summary> /// 購物車 /// </summary> public class CartController : Controller { private readonly IBookRepository _bookRepository; public CartController(IBookRepository bookRepository) { _bookRepository = bookRepository; } /// <summary> /// 首頁 /// </summary> /// <param name="returnUrl"></param> /// <returns></returns> public ViewResult Index(string returnUrl) { return View(new CartIndexViewModel() { Cart = GetCart(), ReturnUrl = returnUrl }); } /// <summary> /// 添加到購物車 /// </summary> /// <param name="id"></param> /// <param name="returnUrl"></param> /// <returns></returns> 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 }); } /// <summary> /// 從購物車移除 /// </summary> /// <param name="id"></param> /// <param name="returnUrl"></param> /// <returns></returns> 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 }); } /// <summary> /// 獲取購物車 /// </summary> /// <returns></returns> 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 <h2>我的購物車</h2> <table class="table"> <thead> <tr> <th>書名</th> <th>價格</th> <th>數量</th> <th>總計</th> </tr> </thead> <tbody> @foreach (var item in Model.Cart.GetCartItems) { <tr> <td>@item.Book.Name</td> <td>@item.Book.Price</td> <td>@item.Quantity</td> <td>@((item.Book.Price * item.Quantity).ToString("C"))</td> </tr> } <tr> <td> </td> <td> </td> <td>總計:</td> <td>@Model.Cart.ComputeTotalValue().ToString("C")</td> </tr> </tbody> </table> <p> <a href="@Model.ReturnUrl">繼續購物</a> </p>
我想,這一定是一個令人激動的時刻,因為我們已經完成了這個基本的添加到購物車的功能。
三、創建一個分部視圖 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); } <div class="pager"> @Html.PageLinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory })) </div>
_BookSummary.cshtml
@model Wen.BooksStore.Domain.Entities.Book <div class="item"> <h3>@Model.Name</h3> @Model.Description <h4>@Model.Price.ToString("C")</h4> @using (Html.BeginForm("AddToCart", "Cart")) { var id = Model.Id; @Html.HiddenFor(x => id); @Html.Hidden("returnUrl", Request.Url.PathAndQuery) <input type="submit" value="+ 添加到購物車" /> } <br /> <hr /> </div>
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.cnblogs.com/liqingwen/p/6647538.html