使用MVC4,Ninject,EF,Moq,構建一個真實的應用電子商務SportsStore(四)


上篇中,我們將數據庫中的數據顯示到了 UI上,在這里我要強調一點,在上篇中我們應用了強類型的View,不要與model業務混淆,有關強類型view的知識點,不在本實例范疇之內,請參閱相關文檔。對於任何一個電子商務網站來說,都需要使用戶能方便的瀏覽所有的商品,並能夠從一頁遷移到另一頁,這是個非常實用、也非常基本的功能,但在MVC4中,怎么實現它呢,現在就讓我們一步一步的完善這個功能。
 
首先,我們要為我們的Product控制器的List 方法添加一個參數,用它來代表瀏覽的頁號,代碼如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{
    public class ProductController : Controller
    {
        private IProductsRepository repository;
        public int PageSize = 4;

        public ProductController(IProductsRepository productRepository)
        {
            this.repository = productRepository;
        }

        public ViewResult List(int page = 1) {

            return View(repository.Products.OrderBy(p => p.ProductID)
                                .Skip((page - 1) * PageSize)
                                .Take(PageSize));
        }

    }
}
PageSize字段指定了每一頁要顯示的產品數量,稍后我們將使用更好的機制來替換它,現在你只需要理解它。
我們還添加了一個可選的參數到List方法,這就表示如果我們調用的方法沒有參數(List()), 我們將會使用(List(1)) 來處理,就是默認為顯示第一頁。
 
現在我們添加一個測試文件到你的SportsStore.UnitTests工程
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Controllers;
using System.Collections.Generic;
using System.Linq;
using SportsStore.WebUI.Models;
using System;

namespace SportsStore.UnitTests {
    [TestClass]
    public class UnitTest1 {
        [TestMethod]
        public void Can_Paginate() {
                // Arrange
                Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
                mock.Setup(m => m.Products).Returns(new Product[] {
                new Product {ProductID = 1, Name = "P1"},
                new Product {ProductID = 2, Name = "P2"},
                new Product {ProductID = 3, Name = "P3"},
                new Product {ProductID = 4, Name = "P4"},
                new Product {ProductID = 5, Name = "P5"}
                }.AsQueryable());
                ProductController controller = new ProductController(mock.Object);
                controller.PageSize = 3;
                // Act
                IEnumerable<Product> result =
                (IEnumerable<Product>)controller.List(2).Model;
                // Assert
                Product[] prodArray = result.ToArray();
                Assert.IsTrue(prodArray.Length == 2);
                Assert.AreEqual(prodArray[0].Name, "P4");
                Assert.AreEqual(prodArray[1].Name, "P5");
        }


    }
}
這里請注意看,我們是如何輕松的從一個控制器的結果集中獲得數據的,在這里,我們反轉了這個結果集到一個數組,並檢查單個對象的長度和值。
運行工程,可以看到如下結果:
 
添加View Model
View Model 不是我們領域模型的一部分,只是為了方便在控制器和View 之間傳遞數據,所以我們把它放在SportsStore.WebUI工程的Models文件夾中,命名為PagingInfo,代碼如下:
using System;

namespace SportsStore.WebUI.Models {

    public class PagingInfo {

        public int TotalItems { get; set; }
        public int ItemsPerPage { get; set; }
        public int CurrentPage { get; set; }

        public int TotalPages
        {
            get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); }
        }

    }
}
添加HTML Helper 方法
現在,我們需要添加一個文件夾命名為HtmlHelpers,並添加一個文件,命名為PagingHelpers。
using System;
using System.Text;
using System.Web.Mvc;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.HtmlHelpers
{
    public static class PagingHelpers
    {
        public static MvcHtmlString PageLinks(this HtmlHelper html,
        PagingInfo pagingInfo,
        Func<int, string> pageUrl)
        {
            StringBuilder result = new StringBuilder();
            for (int i = 1; i <= pagingInfo.TotalPages; i++)
            {
                TagBuilder tag = new TagBuilder("a"); // Construct an <a> tag
                tag.MergeAttribute("href", pageUrl(i));
                tag.InnerHtml = i.ToString();
                if (i == pagingInfo.CurrentPage)
                    tag.AddCssClass("selected");
                result.Append(tag.ToString());
            }
            return MvcHtmlString.Create(result.ToString());
        }
    }
}
這個PageLinks 擴展方法為頁鏈接集合產生HTML,這個頁鏈接集合使用了PagingInfo對象, Func 參數提供了傳遞代理的能力,代理被用來產生鏈接到其他頁面的鏈接。
 
測試我們的HtmlHelpers
在我們測試文件中添加如下引用和方法:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Controllers;
using System.Collections.Generic;
using System.Linq;
using SportsStore.WebUI.Models;
using System;
using System.Web.Mvc;
using SportsStore.WebUI.HtmlHelpers;

namespace SportsStore.UnitTests {
    [TestClass]
    public class UnitTest1 {
        [TestMethod]
        public void Can_Paginate() {
                // Arrange
                Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
                mock.Setup(m => m.Products).Returns(new Product[] {
                new Product {ProductID = 1, Name = "P1"},
                new Product {ProductID = 2, Name = "P2"},
                new Product {ProductID = 3, Name = "P3"},
                new Product {ProductID = 4, Name = "P4"},
                new Product {ProductID = 5, Name = "P5"}
                }.AsQueryable());
                ProductController controller = new ProductController(mock.Object);
                controller.PageSize = 3;
                // Act
                IEnumerable<Product> result =
                (IEnumerable<Product>)controller.List(2).Model;
                // Assert
                Product[] prodArray = result.ToArray();
                Assert.IsTrue(prodArray.Length == 2);
                Assert.AreEqual(prodArray[0].Name, "P4");
                Assert.AreEqual(prodArray[1].Name, "P5");
        }


        [TestMethod]
        public void Can_Generate_Page_Links()
        {
            // Arrange - define an HTML helper - we need to do this
            // in order to apply the extension method
            HtmlHelper myHelper = null;
            // Arrange - create PagingInfo data
            PagingInfo pagingInfo = new PagingInfo
            {
                CurrentPage = 2,
                TotalItems = 28,
                ItemsPerPage = 10
            };
            // Arrange - set up the delegate using a lambda expression
            Func<int, string> pageUrlDelegate = i => "Page" + i;
            // Act
            MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);
            // Assert
            Assert.AreEqual(result.ToString(), @"<a href=""Page1"">1</a>"
            + @"<a class=""selected"" href=""Page2"">2</a>"
            + @"<a href=""Page3"">3</a>");
        }
    }
}
要使擴展方法有效,在代碼中,我們需要確保引用它所在的namespace,我使用了using語句,但是對於一個 Razor View,我們必須配置web.config文件,而一個MVC工程,有2個web.config文件,一個是在工程的根目錄下,另一個在View文件夾中,我們需要配置文件夾中的這個,添加如下語句到namespace標簽中:
<add namespace="SportsStore.WebUI.HtmlHelpers"/>
<namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Optimization"/>
        <add namespace="System.Web.Routing" />
        <add namespace="SportsStore.WebUI.HtmlHelpers"/>
      </namespaces>

 

 
添加View Model數據
我們並沒有打算完全使用HTML helper方法,我們依然需要提供一個PagingInfo view model類的實例到View,我們可以使用view bag的特性,但我們更傾向於打包所有的Controller數據發送到一個單一的View,要實現這一點,我們要添加一個新類到Model文件夾,命名為ProductsListViewModel。
using System.Collections.Generic;
using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Models {

    public class ProductsListViewModel {

        public IEnumerable<Product> Products { get; set; }
        public PagingInfo PagingInfo { get; set; }
    }
}

好了,現在我們再更新一下ProductController的代碼,用ProductsListViewModel類提供給View更詳細的數據。

 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{
    public class ProductController : Controller
    {
        private IProductsRepository repository;
        public int PageSize = 4;

        public ProductController(IProductsRepository productRepository)
        {
            this.repository = productRepository;
        }

        public ViewResult List(int page = 1) {

            ProductsListViewModel model = new ProductsListViewModel
            {
                Products = repository.Products
                                    .OrderBy(p => p.ProductID)
                                    .Skip((page - 1) * PageSize)
                                    .Take(PageSize),
                                    PagingInfo = new PagingInfo
                                    {
                                        CurrentPage = page,
                                        ItemsPerPage = PageSize,
                                        TotalItems = repository.Products.Count()
                                    }
            };
            return View(model);
        }

    }
}


修改測試文件代碼:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Controllers;
using System.Collections.Generic;
using System.Linq;
using SportsStore.WebUI.Models;
using System;
using System.Web.Mvc;
using SportsStore.WebUI.HtmlHelpers;

namespace SportsStore.UnitTests {
    [TestClass]
    public class UnitTest1 {
        [TestMethod]
        public void Can_Paginate() {
                // Arrange
                Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
                mock.Setup(m => m.Products).Returns(new Product[] {
                new Product {ProductID = 1, Name = "P1"},
                new Product {ProductID = 2, Name = "P2"},
                new Product {ProductID = 3, Name = "P3"},
                new Product {ProductID = 4, Name = "P4"},
                new Product {ProductID = 5, Name = "P5"}
                }.AsQueryable());
                ProductController controller = new ProductController(mock.Object);
                controller.PageSize = 3;

                // Action
                ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;

                // Assert
                Product[] prodArray = result.Products.ToArray();
                Assert.IsTrue(prodArray.Length == 2);
                Assert.AreEqual(prodArray[0].Name, "P4");
                Assert.AreEqual(prodArray[1].Name, "P5");
        }

        [TestMethod]
        public void Can_Send_Pagination_View_Model()
        {
            // Arrange
            Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
            mock.Setup(m => m.Products).Returns(new Product[] {
                                                            new Product {ProductID = 1, Name = "P1"},
                                                            new Product {ProductID = 2, Name = "P2"},
                                                            new Product {ProductID = 3, Name = "P3"},
                                                            new Product {ProductID = 4, Name = "P4"},
                                                            new Product {ProductID = 5, Name = "P5"}
                                                            }.AsQueryable());
            // Arrange
            ProductController controller = new ProductController(mock.Object);
            controller.PageSize = 3;
            // Act
            ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;
            // Assert
            PagingInfo pageInfo = result.PagingInfo;
            Assert.AreEqual(pageInfo.CurrentPage, 2);
            Assert.AreEqual(pageInfo.ItemsPerPage, 3);
            Assert.AreEqual(pageInfo.TotalItems, 5);
            Assert.AreEqual(pageInfo.TotalPages, 2);
        }

        [TestMethod]
        public void Can_Generate_Page_Links()
        {
            // Arrange - define an HTML helper - we need to do this
            // in order to apply the extension method
            HtmlHelper myHelper = null;
            // Arrange - create PagingInfo data
            PagingInfo pagingInfo = new PagingInfo
            {
                CurrentPage = 2,
                TotalItems = 28,
                ItemsPerPage = 10
            };
            // Arrange - set up the delegate using a lambda expression
            Func<int, string> pageUrlDelegate = i => "Page" + i;
            // Act
            MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);
            // Assert
            Assert.AreEqual(result.ToString(), @"<a href=""Page1"">1</a>"
            + @"<a class=""selected"" href=""Page2"">2</a>"
            + @"<a href=""Page3"">3</a>");
        }
    }
}

 

運行程序,你將看到如下結果:
 
改進我們的頁面鏈接
我們的頁面鏈接都是工作的,但它看起來是這樣的:
http://localhost/?page=2
我們能做的更好些, 尤其是通過創建一個scheme,它遵循可組裝URLs. 這使得用戶更容易理解,並且更有效率,它看起來應該像下面的地址:
http://localhost/Page2
MVC很容易去改變URL scheme,因為它使用了ASP.NET的routing特性,我們要做的就是添加一個新的route 到RouteConfig.cs文件的RegisterRoutes,打開App_Start文件夾,找到我們要改的文件。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace SportsStore.WebUI
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: null,
                url: "Page{page}",
                defaults: new { Controller = "Product", action = "List" }
                );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional }
            );
        }
    }
}

把我的Route放在默認的Route前是很重要的,Route的處理是按照它們被列出的順序進行的,我們需要用我們新的Route優先於默認的,現在你暫時先了解這些,以后,我們會更加詳細的講解它。

 好了,這個博客的編輯器太難用了,總是不停的刷新,無法固定頁面位置,今天的內容比較多,希望大家能仔細看好每一步,這里面沒有一個字是多余的!剩下沒寫完的下次再寫吧!請繼續關注我的續篇。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM