一步步打造簡(jiǎn)單的MVC電商網(wǎng)站BooksStore(2)
一步步打造一個(gè)簡(jiǎn)單的 MVC 電商網(wǎng)站 - BooksStore(二)
本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore
《一步步打造一個(gè)簡(jiǎn)單的 MVC 電商網(wǎng)站 - BooksStore(一)》
《一步步打造一個(gè)簡(jiǎn)單的 MVC 電商網(wǎng)站 - BooksStore(二)》
《一步步打造一個(gè)簡(jiǎn)單的 MVC 電商網(wǎng)站 - BooksStore(三)》
《一步步打造一個(gè)簡(jiǎn)單的 MVC 電商網(wǎng)站 - BooksStore(四)》
簡(jiǎn)介
上一次我們嘗試了:創(chuàng)建項(xiàng)目架構(gòu)、創(chuàng)建域模型實(shí)體、創(chuàng)建單元測(cè)試、創(chuàng)建控制器與視圖、創(chuàng)建分頁(yè)和加入樣式,而這一節(jié)我們會(huì)完成兩個(gè)功能,分類導(dǎo)航與購(gòu)物車。
主要功能與知識(shí)點(diǎn)如下:
分類、產(chǎn)品瀏覽、購(gòu)物車、結(jié)算、CRUD(增刪改查) 管理、發(fā)郵件、分頁(yè)、模型綁定、認(rèn)證過濾器和單元測(cè)試等(預(yù)計(jì)剩余兩篇,預(yù)計(jì)明天(因?yàn)橹芰环偶伲┖椭苋ㄒ驗(yàn)橹芏簧习啵┌l(fā)布)。
【備注】項(xiàng)目使用 VS2015 + C#6 進(jìn)行開發(fā),有問題請(qǐng)發(fā)表在留言區(qū)哦,還有,頁(yè)面長(zhǎng)得比較丑,請(qǐng)見諒。
目錄
添加分類導(dǎo)航
加入購(gòu)物車
創(chuàng)建一個(gè)分部視圖 Partial View
一、添加分類導(dǎo)航
上一次我們把網(wǎng)頁(yè)劃分成了三個(gè)模塊,其中左側(cè)欄的部分尚未完成,左側(cè)欄擁有將書籍分類展示的功能。

圖 1
1.回到之前的BookDetailsViewModels 視圖模型,我們額外再添加一個(gè)新的屬性用作分類(CurrentCategory):
/// <summary>
/// 書籍詳情視圖模型
/// </summary>
public class BookDetailsViewModels : PagingInfo
{
public IEnumerable<Book> Books { get; set; }
/// <summary>
/// 當(dāng)前分類
/// </summary>
public string CurrentCategory { get; set; }
}
2.修改完視圖模型,現(xiàn)在就應(yīng)該修改對(duì)應(yīng)的 BookController 中的Details 方法

/// <summary>
/// 詳情
/// </summary>
/// <param name="category">分類</param>
/// <param name="pageIndex">頁(yè)碼</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);
}
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;
}
/// <summary>
/// 詳情
/// </summary>
/// <param name="category">分類</param>
/// <param name="pageIndex">頁(yè)碼</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);
}
}
}
參數(shù)增加了一個(gè) category,用于獲取分類的字符串,對(duì)應(yīng) Books 中的屬性的賦值語(yǔ)句改為_bookRepository.Books.Where(x => category == null || x.Category == category),這里的 Lambda 表達(dá)式x => category == null || x.Category ==category 的意思是,分類字符串為空就取庫(kù)中所有的 Book 實(shí)體,不為空時(shí)根據(jù)分類進(jìn)行對(duì)集合進(jìn)行篩選過濾。
還要對(duì)屬性 CurrentCategory 進(jìn)行賦值。
別忘了,因?yàn)榉猪?yè)是根據(jù) TotalItems 屬性進(jìn)行的,所以還要修改地方_bookRepository.Books.Count(x => category == null || x.Category == category),通過 LINQ 統(tǒng)計(jì)不同分類情況的個(gè)數(shù)。
3.該控制器對(duì)應(yīng)的 Details.cshtml 中的分頁(yè)輔助器也需要修改,添加新的路由參數(shù):
<div class="pager">
@Html.PageLinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory }))
</div>
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>
<br />
<hr />
</div>
}
<div class="pager">
@Html.PageLinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory }))
</div>
4.路由區(qū)域也應(yīng)當(dāng)修改一下
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.現(xiàn)在新建一個(gè)名為 NavController 的控制器,并添加一個(gè)名為Sidebar 的方法,專門用于渲染左側(cè)邊欄。

不過返回的 View 視圖類型變成 PartialView 分部視圖類型:
public PartialViewResult Sidebar(string category = null)
{
var categories = _bookRepository.Books.Select(x => x.Category).Distinct().OrderBy(x => x);
return PartialView(categories);
}
在方法體在右鍵,添加一個(gè)視圖,勾上創(chuàng)建分部視圖。

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 框架具有一種叫作“子動(dòng)作(Child Action)”的概念,可以適用于重用導(dǎo)航控件之類的東西,使用類似 RenderAction() 的方法,在當(dāng)前的視圖中輸出指定的動(dòng)作方法。
因?yàn)樾枰诟敢晥D中呈現(xiàn)另一個(gè) Action 中的分部視圖,所以原來的_Layout.cshtml布局頁(yè)修改如下:

現(xiàn)在,啟動(dòng)的結(jié)果應(yīng)該和圖 1 是一樣的,嘗試點(diǎn)擊左側(cè)邊欄的分類,觀察主區(qū)域的變化情況。
二、加入購(gòu)物車

圖 2
界面的大體功能如圖 2,在每本圖書的區(qū)域新增一個(gè)鏈接(添加到購(gòu)物車),會(huì)跳轉(zhuǎn)到一個(gè)新的頁(yè)面,顯示購(gòu)物車的詳細(xì)信息 - 購(gòu)物清單,也可以通過“結(jié)算”鏈接跳轉(zhuǎn)到一個(gè)新的頁(yè)面。
購(gòu)物車是應(yīng)用程序業(yè)務(wù)域的一部分,因此,購(gòu)物車實(shí)體應(yīng)該為域模型。

1.添加兩個(gè)類:
Cart.cs 有添加、移除、清空和統(tǒng)計(jì)功能:
/// <summary>
/// 購(gòu)物車
/// </summary>
public class Cart
{
private readonly List<CartItem> _cartItems = new List<CartItem>();
/// <summary>
/// 獲取購(gòu)物車的所有項(xiàng)目
/// </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>
/// 清空購(gòu)物車
/// </summary>
public void Clear()
{
_cartItems.Clear();
}
/// <summary>
/// 統(tǒng)計(jì)總額
/// </summary>
/// <returns></returns>
public decimal ComputeTotalValue()
{
return _cartItems.Sum(x => x.Book.Price * x.Quantity);
}
}
CartItem.cs 表示購(gòu)物車中的每一項(xiàng):
/// <summary>
/// 購(gòu)物車項(xiàng)
/// </summary>
public class CartItem
{
/// <summary>
/// 書
/// </summary>
public Book Book { get; set; }
/// <summary>
/// 數(shù)量
/// </summary>
public int Quantity { get; set; }
}
2.修改一下之前的 Details.cshtml,增加“添加到購(gòu)物車”的按鈕:
@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="+ 添加到購(gòu)物車" />
}
<br />
<hr />
</div>
}
<div class="pager">
@Html.PageLinks(Model, x => Url.Action("Details", new { pageIndex = x, category = Model.CurrentCategory }))
</div>
【備注】@Html.BeginForm() 方法默認(rèn)會(huì)創(chuàng)建一個(gè) Post 請(qǐng)求方法的表單,為什么不直接使用 Get 請(qǐng)求呢,HTTP 規(guī)范要求,會(huì)引起數(shù)據(jù)變化時(shí)不要使用 Get 請(qǐng)求,將產(chǎn)品添加到一個(gè)購(gòu)物車明顯會(huì)出現(xiàn)新的數(shù)據(jù)變化,所以,這種情形不應(yīng)該使用 Get 請(qǐng)求,直接顯示頁(yè)面或者列表數(shù)據(jù),這種請(qǐng)求才應(yīng)該使用 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.再添加一個(gè) CartController

/// <summary>
/// 購(gòu)物車
/// </summary>
public class CartController : Controller
{
private readonly IBookRepository _bookRepository;
public CartController(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
/// <summary>
/// 首頁(yè)
/// </summary>
/// <param name="returnUrl"></param>
/// <returns></returns>
public ViewResult Index(string returnUrl)
{
return View(new CartIndexViewModel()
{
Cart = GetCart(),
ReturnUrl = returnUrl
});
}
/// <summary>
/// 添加到購(gòu)物車
/// </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>
/// 從購(gòu)物車移除
/// </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>
/// 獲取購(gòu)物車
/// </summary>
/// <returns></returns>
private Cart GetCart()
{
var cart = (Cart)Session["Cart"];
if (cart != null) return cart;
cart = new Cart();
Session["Cart"] = cart;
return cart;
}
}
【備注】這里的購(gòu)物車是通過 Session 會(huì)話狀態(tài)進(jìn)行保存用戶的 Cart 對(duì)象。當(dāng)會(huì)話過期(典型的情況是用戶很長(zhǎng)時(shí)間沒有對(duì)服務(wù)器發(fā)起任何請(qǐng)求),與該會(huì)話關(guān)聯(lián)的數(shù)據(jù)就會(huì)被刪除,這就意味著不需要對(duì) Cart 對(duì)象進(jìn)行生命周期的管理。
【備注】RedirectToAction() 方法:將一個(gè) HTTP 重定向的指令發(fā)給客戶端瀏覽器,要求瀏覽器請(qǐng)求一個(gè)新的 Url。
5.在 Index 方法中選擇右鍵新建視圖,專門用于顯示購(gòu)物清單:

Index.cshtml 中的代碼:
@model Wen.BooksStore.WebUI.Models.CartIndexViewModel
<h2>我的購(gòu)物車</h2>
<table class="table">
<thead>
<tr>
<th>書名</th>
<th>價(jià)格</th>
<th>數(shù)量</th>
<th>總計(jì)</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>總計(jì):</td>
<td>@Model.Cart.ComputeTotalValue().ToString("C")</td>
</tr>
</tbody>
</table>
<p>
<a href="@Model.ReturnUrl">繼續(xù)購(gòu)物</a>
</p>
我想,這一定是一個(gè)令人激動(dòng)的時(shí)刻,因?yàn)槲覀円呀?jīng)完成了這個(gè)基本的添加到購(gòu)物車的功能。

三、創(chuàng)建一個(gè)分部視圖 Partial View
分部視圖,是嵌入在另一個(gè)視圖中的一個(gè)內(nèi)容片段,并且可以跨視圖重用,這有助于減少重復(fù),尤其需要在多個(gè)地方需要重復(fù)使用相同的數(shù)據(jù)時(shí)。

在 Shared 內(nèi)部新建一個(gè)名為_BookSummary.cshtml 的視圖,并且把之前Details.cshtml 的代碼進(jìn)行整理。

修改后的兩個(gè)視圖:
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="+ 添加到購(gòu)物車" />
}
<br />
<hr />
</div>
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- MVC+EasyUI+三層新聞網(wǎng)站建立 建站準(zhǔn)備工作(一)
- MVC+EasyUI+三層新聞網(wǎng)站建立 主頁(yè)布局的方法(五)
- MVC+EasyUI+三層新聞網(wǎng)站建立 實(shí)現(xiàn)登錄功能(四)
- MVC+EasyUI+三層新聞網(wǎng)站建立 后臺(tái)登錄界面的搭建(二)
- MVC+EasyUI+三層新聞網(wǎng)站建立 驗(yàn)證碼生成(三)
- 一步步打造簡(jiǎn)單的MVC電商網(wǎng)站BooksStore(1)
- MVC4制作網(wǎng)站教程第四章 更新欄目4.3
- MVC4制作網(wǎng)站教程第四章 瀏覽欄目4.2
- MVC4制作網(wǎng)站教程第四章 添加欄目4.1
- MVC+EasyUI+三層新聞網(wǎng)站建立 tabs標(biāo)簽制作方法(六)
相關(guān)文章
Image顯示服務(wù)器上任意絕對(duì)路徑下的圖片(采用二進(jìn)制流實(shí)現(xiàn))
有這樣一個(gè)需求:數(shù)據(jù)庫(kù)中存儲(chǔ)的是照片所在的絕對(duì)路徑(可以不在系統(tǒng)所在路徑下),Image控件動(dòng)態(tài)加載路徑下的圖片,另類實(shí)現(xiàn)方法,感興趣的朋友可以參考下,或許本文對(duì)你學(xué)習(xí)二進(jìn)制流有所幫助2013-02-02
.Net平臺(tái)開發(fā)實(shí)踐的一些點(diǎn)滴總結(jié)(技術(shù)規(guī)范與實(shí)踐精華)
以下是本人對(duì).Net平臺(tái)開發(fā)實(shí)踐的一些點(diǎn)滴總結(jié)。這里的技術(shù)規(guī)范主要是開發(fā)過程的代碼規(guī)范、數(shù)據(jù)庫(kù)設(shè)計(jì)規(guī)范、Com和.Net互操作規(guī)范;實(shí)踐精華是對(duì)技術(shù)實(shí)踐過程中的部分總結(jié)。2010-04-04
Asp.Net 5分鐘實(shí)現(xiàn)網(wǎng)頁(yè)實(shí)時(shí)監(jiān)控
在項(xiàng)目開發(fā)中經(jīng)常會(huì)用到監(jiān)控功能,下面通過本篇文章給大家介紹Asp.Net 5分鐘實(shí)現(xiàn)網(wǎng)頁(yè)實(shí)時(shí)監(jiān)控,需要的朋友可以參考下2017-12-12
ASP.NET中實(shí)時(shí)圖表的實(shí)現(xiàn)方法分享
這篇文章介紹了ASP.NET中實(shí)時(shí)圖表的實(shí)現(xiàn)方法,有需要的朋友可以參考一下2013-11-11
asp.net Application_AcquireRequestState事件,導(dǎo)致Ajax客戶端不能加載
項(xiàng)目中使用Application_AcquireRequestState事件,來做一些用戶信息的驗(yàn)證工作.2010-03-03
asp.net關(guān)于Cookie跨域(域名)的問題
Cookie是一個(gè)偉大的發(fā)明,它允許Web開發(fā)者保留他們的用戶的登錄狀態(tài)。但是當(dāng)你的站點(diǎn)有一個(gè)以上的域名時(shí)就會(huì)出現(xiàn)問題了。在Cookie規(guī)范上說,一個(gè)cookie只能用于一個(gè)域名,不能夠發(fā)給其它的域名。因此,如果在瀏覽器中對(duì)一個(gè)域名設(shè)置了一個(gè)cookie,這個(gè)cookie對(duì)于其它的域名將無效。如果你想讓你的用戶從你的站點(diǎn)中的其中一個(gè)進(jìn)行登錄,同時(shí)也可以在其它域名上進(jìn)行登錄,這可真是一個(gè)大難題。2012-12-12
asp.net aspnetpager分頁(yè)統(tǒng)計(jì)時(shí)與實(shí)際不符的解決辦法
最近分頁(yè)方面根據(jù)實(shí)際需要修改了一些函數(shù)2008-11-11

