作者:[美]Adam Freeman 來源:《精通ASP.NET MVC 4》 前面建立的都是簡單的MVC程式,現在到了吧所有事情綜合在一起,以建立一個簡單但真實的電子商務應用程式的時候了。 在此打算建立的應用程式 — SportsStore (體育用品商店),將遵循隨處可見的線上商店所採取的經典方 ...
作者:[美]Adam Freeman 來源:《精通ASP.NET MVC 4》
前面建立的都是簡單的MVC程式,現在到了吧所有事情綜合在一起,以建立一個簡單但真實的電子商務應用程式的時候了。
在此打算建立的應用程式 — SportsStore (體育用品商店),將遵循隨處可見的線上商店所採取的經典方式。將創建一個客戶可以通過分類和頁面進行瀏覽的線上產品分類,一個客戶可以添加和刪除商品的購物車,和一個客戶能夠輸入其右擊地址細節的結算頁面。另外,還將創建一個包含創建、讀取、更新和刪除功能的管理區,以便對產品分類進行管理並對該區域進行保護,以使只有登錄的管理員才能進行修改。
此書打算建立的這個應用程式不只是一個膚淺的演示,而是要創建一個堅固且真實的、符合當前最實用要求的應用程式。由於要建立必要的底層結構,一開始的進度會有點慢。的確,若使用 WebForm,則可以更快地建立最初的功能,只要拖放一些與資料庫直接綁定的空間即可。但在 MVC 應用程式中所付出的這些初期工作,會帶來可維護、可擴展以及結構良好的代碼,且這些代碼對單元測試具有卓越支持。一旦恰當地建好了這種基本的底層架構,後面的事情就會快起來了。
1.開始
1.1 創建 Visual Studio 解決方案和項目
本文打算創建一個含有三個項目的Visual Studio 解決方案,一個項目包含域模型,一個是MVC 應用程式,第三個則包含單元測試。首先用"空白解決方案"模板創建一個名為"SportsStore"的新的Visual Studio 解決方案。
Visual Studio 解決方案是一個含有一個或多個項目的容器。示例應用程式需要三個項目,如下圖所示:
1.2 添加引用
參考前面的博文 【MVC 4】3.MVC 基本工具(創建示例項目、使用 Ninject) 和 【MVC 4】4.MVC 基本工具(Visual Studio 的單元測試、使用Moq) 對庫和項目做好正確的引用。所需的項目依賴性如下圖所示:
1.3 設置DI容器
之前的文章 【MVC 4】3.MVC 基本工具(創建示例項目、使用 Ninject) 展示過如何使用 Ninject 創建一個自定義依賴性解析器,以便 MVC 框架用它創建整個應用程式實例化對象。這裡打算採用不同的方法,即創建一個自定義的控制器工廠。用戶可以在其中添加自定義代碼,以改變MVC框架的(預設)行為,或者像這裡所做的一樣,將DI 限制到應用程式的一部分。常用的模式是用依賴性解析器來處理 DI,而用自定義控制器工廠來改變查找控制器類的方式,但在此例中,只打算使用控制器工廠。
在 SportsStore.WebUI 項目中創建一個名稱"Infreastructure"的文件夾,然後創建一個名為"NinjectContrillerFactory (Ninject 控制器工廠)"的類。
using Ninject; using System; using System.Web.Mvc; using System.Web.Routing; namespace SportsStore.WebUI.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //put bindings here } } }
雖然沒有添加任何綁定,但在需要時,可以使用AddBindings 方法。必須告訴 MVC 希望使用此 NinjectControllerFactory 類來創建控制器對象,其辦法是在 SportsStore.WebUI 項目中 Global.asax.cs 文件的Application_Start 方法中添加一些代碼,如下粗體部分所示:
using SportsStore.WebUI.Infrastructure;using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace SportsStore.WebUI { // 註意: 有關啟用 IIS6 或 IIS7 經典模式的說明, // 請訪問 http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); } } }
1.4 運行程式
運行程式,會看到一個錯誤頁面,這是因為所請求的 URL 是與 Ninject 尚未進行綁定的控制器相關聯的:
如果進行到這一步,說明 Visual Studio 2012 和ASP.NET MVC 開發環境的準備工作進行的十分順利。
2.從域模型開始
MVC 應用程式中有太多的事情都是圍繞域模型而展開的,因此,域模型是開始工作的最佳位置。
由於這是一個電子商務應用程式,因此需要的最明顯的域實體是產品(Product)。在SportsSore.Domain 項目中創建一個名為 "Entities" 的新文件夾,然後在其中創建一個名為“Product”的類。
namespace SportsStore.Domain.Entities { public class Product { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { get; set; } } }
上述清單遵循了在一起獨立的 Visual Studio 項目中定義域模型的約定,即類必須標記為 public,雖然不一定要遵循這一約定,但這麼做有助於保持模型與控制器分離。
2.1 創建一個抽象的存儲庫
現在需要某種方式來獲取資料庫中的Product 實體。正如前面博文所解釋的,人們希望持久化邏輯與域模型實體是分離的——此事通過使用存儲庫模式來實現。此刻,不必擔心會如何實現持久化,不過,將從定義它的介面來開始這一過程。
在 SportsStore.Domain 項目中創建一個名為 Abstract 的頂層新文件夾,並創建一個名為 IProductRepository 的新介面,代碼如下:
using SportsStore.Domain.Entities; using System.Linq; namespace SportsStore.Domain.Abstract { public interface IProductRepository { IQueryable<Product> Products { get; } } }
該介面使用了 IQueryable<T> 介面,以便能夠獲得一系列 Product 對象,而不必說明數據如何存儲、存儲在哪兒,以及如何接收數據。使用這一 IProductRepository 介面的類,可以獲取 Product 對象而不必知道它們來自哪兒或如何遞交它們,這是存儲庫模式的本質。在添加特性的整個開發過程中,將重新審視這一介面。
2.2 創建模仿存儲庫
現在,已經定義了一個抽象介面,可以實現持久化機制,並將其掛接到一個資料庫。本文打算在後面部分再做這件事情。為了能夠開始編寫應用程式的其他部分,本文打算創建一個 IProductRepository 介面的模仿實現。本文打算在 SportsStore.WebUI 項目的 NinjectControllerFactory 類的 AddBindings 方法中做這件事,代碼如下:
using Moq; using Ninject; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System; using System.Linq; using System.Collections.Generic; using System.Web.Mvc; using System.Web.Routing; namespace SportsStore.WebUI.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //put bindings here Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m=>m.Products).Returns(new List<Product>{ new Product{Name="Football",Price=25}, new Product{Name="Surf board",Price=179}, new Product{Name="Running shoes",Price=95} }.AsQueryable()); ninjectKernel.Bind<IProductRepository>().ToConstant(mock.Object); } } }
此處必須對該問卷添加一些命名空間,但用來創建模仿存儲庫實現的過程使用的是 【MVC 4】4.MVC 基本工具(Visual Studio 的單元測試、使用Moq) 所介紹的同樣的 Moq 技術。AsQueryable方法是一個 LINQ 擴展方法,它將 IEnumerable<T> 轉換成 IQueryable<T>,此處需要它來匹配介面簽名。
人們希望,Ninject 無論何時接收到一個 IProductRepository 介面實現的請求,都返回同樣的模仿對象,這便是使用 ToConstant 方法的原因
... ninjectKernel.Bind<IProductRepository>().ToConstant(mock.Object); ...
Ninject 會一直以該模仿對象來滿足對 IProductRepository 介面的請求,而不是每次都創建一個新的實現對象實例。
3.顯示產品列表
本小結將創建一個控制器和一個動作方法,它能夠顯示存儲庫中的產品細節。此刻,將只只針對模仿存儲庫中的數據。
3.1 添加控制器
新建控制器"ProductController",模板為"空 MVC 控制器",修改代碼如下
using SportsStore.Domain.Abstract; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductRepository repository; public ProductController(IProductRepository productRepository) { this.repository = productRepository; } public ViewResult List() { return View(repository.Products); } } }
像這樣調用 View 方法(未指定視圖名稱),是告訴框架為該動作方法渲染一個預設視圖。通過將 Product 對象的列表傳遞給這個 View 方法,這是在給框架提供數據,以便用這些數據填充強類型視圖中的 Model 對象。
3.2 添加視圖
現在需要為 List 動作方法添加預設視圖。添加對應的視圖文件 List.cshtml ,並渲染視圖文件如下:
@model IEnumerable<SportsStore.Domain.Entities.Product> @{ ViewBag.Title = "Products"; } @foreach (var p in Model) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }
3.3 設置預設路由
現在要做的全部工作是告訴 MVC 框架,抵達網站根的請求應該被映射到 ProductController 類的List 動作方法上。這可以通過編輯 Global.asax.cs 的 RegisterRoutes 方法實現,代碼如下:
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: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional } ); } } }
3.4 運行應用程式
至此,所有基礎工作均已就緒。此刻已經有一個含有一個動作方法的控制器,該動作方法在預設 URL 被請求時被調用,它依賴於存儲庫介面的一個模仿實現,該存儲庫介面生成了一些簡單的測試數據。這些測試數據被傳遞給與動作方法關聯在一起的視圖,而視圖對每個產品創建一個簡單的細節列表。運行該應用程式,效果圖如下:
這是ASP.NET MVC 框架典型的開發模式。
4.準備資料庫
前面已經可以顯示含有產品細節的簡單視圖,但其顯示的知識模仿的 IProductRepository 所返回的測試數據。在可以顯示真實數據的存儲庫之前,還需要建立一個資料庫,並用一些數據填充它。
本文打算以 SQL Server 作為資料庫,並用 Entity Framework(實體框架 - EF)來訪問資料庫,EF 是 .NET 的ORM(對象關係映射)框架。ORM 框架讓開發人員可以用規則的C# 對象來使用關係資料庫的表、列和行。 LINQ 可以與不同的數據源一起工作,其中之一就是 Entity Framework 。
4.1 創建資料庫
補充到 Visual Studio 2012 和 SQL Server 2012 中的一個很好的特性是 LocalDB。它是特別為開發者而設計的一個免管理的SQL Server 核心功能實現。使用該特性使我們在建立項目、以及後面將資料庫部署到完整版的 SQL Server 期間,可以跳過資料庫的設置過程。
第一個步驟是在 Visual Studio 中創建資料庫連接。從"View(視圖)"菜單中打開"Database Explorer(資料庫資源管理器)"視窗,點擊”Connect to Database(連接到資料庫)“按鈕。
根據提示,登錄資料庫,並新建資料庫 SportsStore
4.2 定義資料庫方案
新建的資料庫只需要一個數據表,用以存儲 Product 數據。右擊資料庫對應的"Tables(表)"條目,新增數據表。
這將會顯示創建新表的設計器。使用 T-SQL 視窗,輸入對應的SQL語句創建數據表 Products 。
點擊左上角的"更新"按鈕,會看到該語句的效果摘要。
點擊"更新資料庫",以執行該 SQL 語句,併在資料庫中創建 Products 表。
4.3 向資料庫添加數據
本文打算對該資料庫手工添加一些數據。
在“資料庫資源管理器”視窗中,展開 SportsStore 資料庫的“表”條目,右擊 Products 表,選擇“顯示表數據”,然後輸入下圖所示數據。可以用 Tab 鍵逐行移動游標。在一行的最後按 Tab 鍵,將移到下一行並更新資料庫中的數據。
4.4 創建實體框架上下文
Entity Framework 的最新版包含了一個叫做“Code-first(代碼先行)”的很好的特性。其思想是可以先定義模型中的類,然後再通過這些類生成資料庫。
這很適合綠地(Green-field)開發項目,但這些項目並不多見。因此,本文打算演示下 Code-First 的一種變異,以此把模型類與現有的資料庫關聯在一起。
第一步,是將Entity Framework (此處是6.1版本)添加到 SportsStore.Domain 項目中。通過 管理NuGet 包,安裝最新的 Entity Framework 包。
下一個步驟是創建一個將前面建立的簡單模型與資料庫關聯起來的上下文類(Context Class)。
創建一個新文件夾 "Concrete",併在其中添加一個名為"EFDbContext"的新類。代碼如下:
using SportsStore.Domain.Entities; using System.Data.Entity; namespace SportsStore.Domain.Concrete { public class EFDbContext:DbContext { public DbSet<Product> Products { get; set; } } }
為了利用 Code-First 特性,需要創建一個派生於 System.Data.Entity.DbContext 的類。這個類會為用戶要使用的資料庫中的每個表自動地定義一個屬性。
該屬性指定了表名,並把 DbSet 結果的類型參數指定為實體框架用來表示表行的模型。在這個例子中,該屬性名是 Products(資料庫中的表名)。即,希望用 Product 模型類型來表示 Products 表的各個行。
需要告訴 Entity Framework 如何連接到資料庫,為了完成這一工作,只需要在 SportsStore.WebUI 項目的 Web.config 文件中以上下文類同樣的名字添加一條資料庫連接字元串即可,如下所示
<connectionStrings> <add name="EFDbContext" connectionString="Data Source=(localdb)\v11.0;Initial Catalog=SportsStore;Integrated Security=True"
providerName="System.Data.SqlClient" /> </connectionStrings>
4.5 創建 Product 存儲庫
現在,本文已經做好了真正實現 IProductRepository 類所需要的各種準備。 在 SportsStore.Domain 項目的 Concrete 文件夾中添加一個類,取名"EFProductRepository",代碼如下:
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System.Linq; namespace SportsStore.Domain.Concrete { public class EFProductRepository:IProductRepository { private EFDbContext context = new EFDbContext(); public IQueryable<Product> Products { get { return context.Products; } } } }
這就是存儲庫類,它實現了 IProductRepository 介面,並使用了一個 EFDbContext 實例,以便用 Entity Framework 接收資料庫的數據。在對該存儲庫添加特性時,便會看到此處是如何使用 Entity Framework 的。
最後一步是吧 Ninject 對模仿存儲庫的綁定替換為對實際存儲庫的綁定。編輯 SportsStore.WebUI 項目中的 NinjectControllerFactory 類,使 AddBindings 方法如下所示:
using Moq; using Ninject; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System; using System.Linq; using System.Collections.Generic; using System.Web.Mvc; using System.Web.Routing; using SportsStore.Domain.Concrete; namespace SportsStore.WebUI.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //put bindings here ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>(); } } }
新的綁定以粗體顯示,它告訴我 Ninject ,用戶希望創建 EFProductRepository 類的實例來對 IProductRepository 介面的請求進行服務。再次運行應用程式,效果如下:
註意:記得更新 SportsStore.WebUI 項目中 Entity Framework的版本,與 SportsStore.Domain 項目中的版本保持一致。不然報錯。
5.添加分頁
從上圖可以看出,資料庫中的所有產品都顯示在一個單一的頁面上。本小結將添加對分頁的支持,以便在一個頁面上顯示一定數目的產品,用戶可以逐頁查看整個產品分類。要實現這一點,可以在 Product 控制器中的 List 方法上添加一個參數,如下所示:
using SportsStore.Domain.Abstract; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductRepository repository; //指明用戶希望每頁顯示4個產品 public int PageSize = 4; public ProductController(IProductRepository productRepository) { this.repository = productRepository; } public ViewResult List(int page=1) { //從存儲庫獲取 Product 對象, //按主鍵順序排序,略過起始頁之前出現的產品數, //然後取出由 PaeSize 欄位指定的產品個數 return View(repository.Products.OrderBy(p=>p.ProductID).Skip((page-1)*PageSize).Take(PageSize)); } } }
5.1 顯示頁面鏈接
如果運行這個應用程式,將看到只有四個條目顯示在頁面上。如果想查看另一頁,可以把查詢字元串參數加到 URL 的末尾,如下所示:
http://localhost:64245/?page=2
需要修改 URL 的埠號,使之與正在運行的 ASP.NET 開發伺服器埠號匹配。運用這種查詢字元串,可以對整個產品分類進行導航。
而為了方便客戶。需要在每個產品列表的底部渲染一些頁面的鏈接,以使客戶可以在不同的頁面之間導航。為了達到這一目的,本文打算實現一個可重用的 HTML 輔助器方法,它類似於之前 【MVC 4】1.第一個 MVC 應用程式 中使用的 Html.TextBoxFor 和 Html.BeginForm 方法。該輔助器方法將為所需要的導航鏈接生成 HTNL 標記。
(1) 添加視圖模型
為了支持 HTML 輔助器方法,本文打算把可用頁面數、當前頁、已經存儲庫中產品總數等方面的信息傳遞給視圖。做這種事最容易的辦法是創建一個視圖模型,在 SportsStore.WebUI 文件夾 Models 中新建類文件 PagingInfo.cs ,代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; 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); } } } }
視圖模型並不是域模型的一部分,它只是一種便於在控制器與視圖之間傳遞數據的類。為了強調這一點,將這個類放在 SportsStore.WebUI 項目中,以使它與域模型的類分離開(將視圖模型放在 MVC 框架項目的 Models 文件夾,而不是放在類庫項目中,這種做法足以說明視圖模型不是域模型,明確了概念,也使應用程式的結構更清晰)。
(2)添加 HTML 輔助器方法
現在有了這個視圖模型,便可以實現這個 HTML 輔助器方法了,該方法稱為“PageLinks”。在 SportsStore.WebUI 項目中創建一個新文件夾“HtmlHelpers”,並添加一個新的靜態類“PagingHelpers(分頁輔助器)”。類文件的內容如下所示:
using SportsStore.WebUI.Models; using System; using System.Text; using System.Web.Mvc; 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"); // 構造一個<a>標簽 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 擴展方法使用 PagingInfo 對象中提供的信息生成一組頁面鏈接的 HTML 。Func 參數提供了在其中傳遞委托的能力,該委托用於生成查看其它頁面的鏈接。
只有包含擴展方法的命名空間在範圍內時,其中的擴展方法才是可用的。在一個代碼文件中,這是用 using 語句來完成的;但對於一個 Razor 視圖,必須把一個配置條目添加到 Web.config 文件中,或在這個視圖上添加一條 @using 語句 。容易混淆的是,在一個 Razor 的 MVC 項目中有兩個 Web.config 文件:主配置文件位於應用程式的根目錄,而視圖專用的配置文件位於 Views 文件夾。需要修改的是 Views/Web.config 文件,如下所示:
<system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <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> </pages> </system.web.webPages.razor>
在一個 Razor 視圖中需要引用的每一個命名空間,都需要以這種方式進行聲明,或在視圖中用 @using 語句進行聲明。
(3)添加視圖模型視圖
目前還沒做好使用 HTML 輔助器方法的準備,還需要把這個 PagingInfo 視圖模型類的一個實例提供給視圖。可以用 View Bag (視圖包)特性來做這件事,但是一個更好的辦法是把控制器發送給視圖所有數據封裝成一個單一的視圖模型類。為此,需要把一個新的名為“ProductsListViewModel” 的類添加到 SportsStore.WebUI 的 Models 文件夾。
using SportsStore.Domain.Entities; using System.Collections.Generic; namespace SportsStore.WebUI.Models { public class ProductsListViewModel { public IEnumerable<Product> Products { get; set; } public PagingInfo PagingInfo { get; set; } } }
現在,可以更新 ProductController 類中的 List 方法,以便使用這個 ProductsListViewModel 類,給視圖提供在頁面上顯示的產品細節和分頁細節,修改後代碼如下:
using SportsStore.Domain.Abstract; using SportsStore.WebUI.Models; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductRepository repository; public int PageSize = 4; public ProductController(IProductRepository 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); } } }
這些修改將一個 ProductsListViewModel 對象作為模型數據傳遞給了視圖。
此時,視圖期望的是一個 Product 對象的序列,因此需要更新 List.cshtml ,以處理這個新視圖模型類型,修改後代碼如下:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }
這個例子修改了 @model 指示符,以告訴 Razor,現在正在使用一個不同的數據類型。也需要更新 foreach 迴圈,以使數據源是模型數據的 Products 屬性。
(4)顯示頁面鏈接
現在已經做到好了再 List 視圖上添加頁面鏈接的所有準備。前面已經創建了含有分頁信息的視圖模型,更新了控制器以使這個信息能夠傳遞給視圖,並修改了 @model 指示符以匹配新的視圖模型類。剩下的事是在視圖中調用這個 HTML 輔助器方法,修改視圖文件如下:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> } <div class="pager"> @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })); </div>
運行該應用程式,可以看到已經添加了頁面鏈接,如下圖所示。這個鏈接的樣式仍然是很基本。重要的是這個鏈接能把客戶從一個頁面帶到另一個頁面,並瀏覽正在銷售的產品。
5.2 改進 URL
頁面鏈接雖然可以其作用,但它們使用的仍然是查詢字元串,以便將分頁信息伺服器,代碼如下:
http://localhost/?page=2
一個更好的方法是專門創建一種遵循可組合 URL 模式的方案。“可組合 URL ”是一種對用戶有意義的方式,其形式如下:
http://localhost/Page2
幸運的是,MVC 很容易修改 URL 方案,因為它使用了 ASP.NET 的路由特性。所要做的知識吧一條新路由添加到 Global.asax.cs 中的 RegisterRoutes 方法,如下所示:
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 } ); } } }
重要的是吧這條路由加在 Default 路由之前。路由是按它們列出的順序進行處理的,這裡需要這條新路由優先於已經存在的那條。
這是唯一需要對產品分頁的 URL 方案進行修改的地方。MVC 框架與路由功能是密切集成的。因此這樣的修改將自動反映在 Url.Action 方法的處理結果中。如果運行這個應用程式,並導航到一個頁面,將會看到這個新的 URL 方案在其作用。
6.設置內容樣式
前面已經建立了大量的基礎結構,而且應用程式也開始真正地集合在一起了,但並未把註意力放到其外觀上。即使這本書不是一本關於 Web 設計或 CSS 的書,但SportsStore 應用程式設計也會因為太糟糕的格式而破壞它的技術強度。本節將一些常規的事情。
本文打算實現一個帶有頭部的經典式兩列佈局。
6.1 定義佈局中的公用內容
【MVC 4】2.使用 Razor 中曾解釋了 Razor 佈局是如何工作和運用的。 當為 Product 控制器創建 List.cshtml 視圖時,曾要求用戶選中 “使用一個佈局” 覆選框,但該文本框保留為空。這便使用了預設佈局 _Layout.cshtml,可以在 SportsStore.WebUI 項目的 Views/Shared 文件夾中找到它。打開這個文件並修改如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" />