• [.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>

      【备注】@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;
        }
    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 ...》

  • 相关阅读:
    Eclipse创建Maven Web项目 + 测试覆盖率 + 常见问题(2015.07.14——湛耀)
    JAVA面试题——JAVA编程题1(2015.07.22)
    Spring 国际化
    BeanPostProcessor 的使用,实现在对象初始化之前或者之后对对象进行操作
    spring 的配置 bean>>property>>name属性
    文件保存
    Spring-demo1(初学者的尝试,2015.03.19)
    新花生壳内网版2.3 + Tomcat7 搭建自己的网站(2015.01.21)
    (原)Struts 相关资源下载
    Xilinx 7 series FPGA multiboot技术的使用(转)
  • 原文地址:https://www.cnblogs.com/liqingwen/p/6647538.html
Copyright © 2020-2023  润新知