构建一个真实的应用电子商务SportsStore4
构建一个真实的应用电子商务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。
好了,现在我们再更新一下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优先于默认的,现在你暂时先了解这些,以后,我们会更加详细的讲解它。
好了,这个博客的编辑器太难用了,总是不停的刷新,无法固定页面位置,今天的内容比较多,希望大家能仔细看好每一步,这里面没有一个字是多余的!剩下没写完的下次再写吧!请继续关注的续篇。
标签: MVC4 , Ninject , EF , Moq , IOC , Jquery
作者: Leo_wl
出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息查看更多关于构建一个真实的应用电子商务SportsStore4的详细内容...