上篇中,我們將數據庫中的數據顯示到了 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文件夾,找到我們要改的文件。
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優先於默認的,現在你暫時先了解這些,以后,我們會更加詳細的講解它。
好了,這個博客的編輯器太難用了,總是不停的刷新,無法固定頁面位置,今天的內容比較多,希望大家能仔細看好每一步,這里面沒有一個字是多余的!剩下沒寫完的下次再寫吧!請繼續關注我的續篇。
