MyException - 我的异常网
当前位置:我的异常网» VC/MFC » [.NET] 一步步制作一个简单的 MVC 电商网站

[.NET] 一步步制作一个简单的 MVC 电商网站

www.MyException.Cn  网友分享于:2013-04-21  浏览:0次
[.NET] 一步步打造一个简单的 MVC 电商网站

一步步打造一个简单的 MVC 电商网站 - BooksStore(二)

  本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore

  《一步步打造一个简单的 MVC 电商网站 - BooksStore(一)》(发布时间:2017-03-30 )

      《一步步打造一个简单的 MVC 电商网站 - BooksStore(二)》(发布时间:2017-03-31)

  《一步步打造一个简单的 MVC 电商网站 - BooksStore(三)》(发布时间:2017-04-01)

  《一步步打造一个简单的 MVC 电商网站 - BooksStore(四)》(发布时间:2017-04-05)

 

简介

  上一次我们尝试了:创建项目架构、创建域模型实体、创建单元测试、创建控制器与视图、创建分页和加入样式,而这一节我们会完成两个功能,分类导航与购物车。

  主要功能与知识点如下:

    分类、产品浏览、购物车、结算、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);
        }
    }
}
BookController.cs

  

  参数增加了一个 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>
Details.cshtml

 

  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 }
            );
        }
RouteConfig.cs

 

  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>

 

[email protected]() 方法默认会创建一个 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;
    }
Site.css

 

  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 

【参考】《精通 ASP.NET MVC ...》

4楼木宛城主
不错,加油
3楼睡了
厉害了
2楼havas
不错,另外 购物车里面好像漏了添加 CartIndexViewModel :)
Re: 反骨仔(二五仔)
@havas,你真细心
1楼反骨仔(二五仔)
今天的人都去哪了,放假了吗
Re: 偏执的光辉岁月
@反骨仔(二五仔),周五无心工作

文章评论

“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
总结2014中国互联网十大段子
总结2014中国互联网十大段子
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
那些争议最大的编程观点
那些争议最大的编程观点
如何成为一名黑客
如何成为一名黑客
编程语言是女人
编程语言是女人
我是如何打败拖延症的
我是如何打败拖延症的
鲜为人知的编程真相
鲜为人知的编程真相
Java程序员必看电影
Java程序员必看电影
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
程序员应该关注的一些事儿
程序员应该关注的一些事儿
代码女神横空出世
代码女神横空出世
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
10个调试和排错的小建议
10个调试和排错的小建议
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
中美印日四国程序员比较
中美印日四国程序员比较
为什么程序员都是夜猫子
为什么程序员都是夜猫子
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
 程序员的样子
程序员的样子
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
程序员和编码员之间的区别
程序员和编码员之间的区别
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
漫画:程序员的工作
漫画:程序员的工作
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
一个程序员的时间管理
一个程序员的时间管理
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
程序员的鄙视链
程序员的鄙视链
我的丈夫是个程序员
我的丈夫是个程序员
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有