我們已經可以顯示簡單的視圖,但是我們仍然是模擬IProductRepository實現返回的是一些測試數據,這個時候我們就需要相應的數據庫來存儲我們項目相關的東西,所以我們需要創建數據庫。我們將使用SQL Server作為數據庫,我們將訪問數據庫使用的實體框架(EF)EntityFramework,這是.Net ORM框架。(ORM框架:稱"對象關系映射",ORM 主要是把數據庫中的關系數據映射稱為程序中的對象).我們使用實體框架有幾個原因。首先,它是簡單和易懂容易上手。第二,用LINQ是意會一流.第三個原因是,它實際上是相當不錯的。早期的版本有一點相對不理想的,但是隨着版本的演變當前版本是非常優雅和功能豐富。
首先,創建數據庫
第一步是創建數據庫,我們要做的,使用內置的數據庫管理工具包括在Visual Studio。打開服務器資源管理器窗口(如下圖1)
圖1.右鍵單擊數據連接並選擇創建新的數據庫從彈出式菜單。進入你的數據庫服務器和名稱的名稱設置新的數據庫以SportStore。如下圖2
圖2.
數據庫創建好之后,接下來就是定義數據模型,我們需要在我們的數據庫創建一張表,將會使用存儲我們的產品數據。添加表及表的數據的步驟一次如下圖3-圖5.
圖3.
圖4.
圖5.
圖5.的數據沒有添加完成,下面的程序還加入了更多的數據。
接着,創建實體框架(Entity Framework )環境
實體框架的4.1版本包括一個很棒的功能,稱為"代碼優先"。我們可以在我們的模型中定義的類,也可以生成一個數據庫映射一個類。添加Entity Fremework到我們的項目,方法是在我們需要添加的項目右鍵,選擇"NuGet程序包管理",然后安裝EF(Entity Frmework)框架到我們的項目,如下圖6.
圖6.項目添加好EF(Entity Framwork)框架后,我們需要一個交互的類讓他來把我們簡單的數據模型來和數據庫交互。然后我們寫這么一個類(EFDbContext),具體代碼如下:
//EFDbContext類可以使簡單數據模型與數據庫交互 public class EFDbContext : DbContext { public DbSet<Product> Products { get; set; } }
代碼相信沒有什么可以解釋的,接着我們需要告訴實體框架如何連接到數據庫,並且我們可以通過添加一個添加一個連接字符串添加到Web.config里面。具體代碼如下:
<connectionStrings> <add name="EFDbContext" connectionString="Data Source=.\XXXX;Initial Catalog=SportsStore;Persist Security Info=True;User ID=sa;Password=123;Pooling=False" providerName="System.Data.SqlClient"/> </connectionStrings>
注:上面紅色部分大家在自己的環境,去配置自己的連接字符串即可,我這里在寫博的時候改成XXX,不要造成什么誤會.
配置和連接字符串EF就能知道怎么讓我們定義的類和數據庫友好的交互了,這個過程實在很妙。接下來我們創建一個儲存商品的庫房,然后在我們的模型域類庫項目(SportsStore.Domain)添加一個文件夾(Concrete)來放我們的存儲商品庫房類(EFProductRepository),它的具體代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using SportsStore.Domain.Abstract; using System.Data.Entity; using SportsStore.Domain.Entities; namespace SportsStore.Domain.Concrete { public class EFProductRepository : IProductRepository { private EFDbContext context = new EFDbContext(); public IQueryable<Product> Products { get { return this.context.Products; } } } //EFDbContext類可以使簡單數據模型與數據庫交互 public class EFDbContext : DbContext { public DbSet<Product> Products { get; set; } } }
這個類,它實現了IProductRepository接口和使用了一個EFDbContext實例他會從數據庫檢索數據使用Entity框架。然后修改我們之前使用Mock模擬IProductRepository綁定的數據,使用真正的數據來綁定,具體代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using Ninject; using System.Web.Routing; using Moq; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.Domain.Concrete; namespace SportsStore.WebUI.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { this.ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //綁定額外數據 this.ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>(); } } }
修改NinjectControllerFactory里的Addbindings方法為上面代碼加粗部分即可。項目到這里就可以跑起來,只不過這次是從數據庫拿的數據,如下圖7(由於當時做到這里時候圖片沒有保存好,所以下面的圖可能是這話結束的圖帶了一點樣式).
圖6.原本應該是我圖中紅色框框的部分,這個是后來加了樣式的,還請大家不要見怪。
接下來不啰嗦,搞分頁,一個頁面顯示過多的商品就顯得很不要好了,所以我們就搞個分頁來讓這種不友好消失在分頁上。修改ProductController給List方法(Action)添加分頁,具體代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Concrete; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { public int PageSize = 4; //設置一頁顯示多少商品 private IProductRepository repository; public ProductController(IProductRepository productReposittory) { this.repository = productReposittory; } //返回一個視圖 public ViewResult List(int page = 1) { return this.View(this.repository.Products.OrderBy(p => p.ProductID) .Skip((page - 1) * PageSize) .Take(PageSize)); } } }
OK,這樣搞是沒錯的,怎么跑起來頁面只有4個商品信息,分頁呢!其實分頁我們已經實現了,那其他的數據呢,我們在地址欄后面繼續輸入,如下圖就可以訪問到其他的數據,如下圖7.
圖7.我們需要這樣的拼寫連接才能訪問,這樣似乎比我們原來直接頁面展示完所有的所有的數據的情況還要糟糕。這樣我們開發人員當然明了其中的原有,那要是換做客戶,他們可是不會知道這個的。所以我們需要在每個頁面的底部顯示出頁面的超連接,用戶通過點擊這個連接可以訪問到不同的頁面,所以我們要實現一個可重用的HTML Help方法供我們使用。我們HTML Help將會生成的HTML標記導航鏈接我們需要。
添加視圖模型
支持HTML Helper類,我們將信息的數量傳遞到視圖的頁面可用,當前頁面,產品在存儲的庫總數量。添加一個視圖模型類(PagingInfo)到Mvc Web項目的Models文件下,具體代碼如下:
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); } } } }
一個視圖模型並不是模型域(Domain)的一部分,這個類只不過方便了控制器(Controller)和視圖(View)之間的數據傳遞。
然后添加HTML Helper方法,既然我們有了視圖模型,我們可以實現HTML helper方法,我們可以使用PageLinks實現數據交互。在我們Mvc Web項目里添加一個文件夾(HtmlHelpers)創建一個類稱為"PagingHelpers"。具體代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.WebUI.Models; using System.Text; 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()); } } }
PageLink擴張方法生成的HTML的一組頁面鏈接,使用信息來自PagingInfo對象中提供。生成的連接可以轉移到其他頁面展示相應頁面的數據信息。
OK,先不急於功能的實現了,我們創建項目的時候不是創建了一個單元測試的模塊,那現在就目前的所寫的代碼,我們可以試着去寫一些的測試方法。來寫一個測試創建頁面鏈接的單元測試,在我們的SportsStore.UnitTests項目里添加我們的測試連接的類(PagingHelpersTest),我們需要給SportsStore.UnitTests項目項目添加2個引用,具體如下圖8.
圖8.添加好單元測試的DLL后繼續PagingHelpersTest類的代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Web.Mvc; using SportsStore.WebUI.HtmlHelpers; using SportsStore.WebUI.Models; namespace SportsStore.UnitTests { [TestClass()] public class PagingHelpersTest { [TestMethod] public void Can_Generate_Page_Links() { //為了應用擴張方法 HtmlHelper myHelper = null; //創建PagingInfo數據 PagingInfo pagingInfo = new PagingInfo { CurrentPage = 2, TotalItems = 28, ItemsPerPage = 10 }; Func<int, string> pageUrlDelegate = i => "Page" + i; MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate); Assert.AreEqual(result.ToString(), @"<a href=""Page1"">1</a><a class=""selected"" href=""Page2"">2</a><a href=""Page3"">3</a>"); } } }
這個單元測試的方法先寫到這里,后面在繼續補充有關單元測試的東東。
接着單元測試之前繼續,一個擴展方法(PagingHelpers類的方法)可供只有當名稱空間,其中包含它在作用域中使用。在每一個代碼文件,都要做了一次使用聲明,但對於一個Razor視圖,我們必須對Web.config文件經行配置,這樣我們就要對我們的Mvc Web項目的Views文件夾下的Web.config經行配置,具體配置如下:
<system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.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.Routing" /> <add namespace="SportsStore.WebUI.HtmlHelpers"/> </namespaces> </pages> </system.web.webPages.razor>
配置如上面紅色加粗的部分.
添加視圖數據模型
我們不准備使用我們的HTML Helper方法。我們目前還沒有提供的一個實例的PagingInfo模型類視圖。視圖數據或視圖包特性,但是我們將需要處理轉換為正確的類型。那么我們包裝所有的數據給控制器和視圖單一的數據類型。我們在Web項目里的Models文件夾下添加一個類(ProductsListViewModel),具體代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Models { public class ProductsListViewModel { public IEnumerable<Product> Products { get; set; } public PagingInfo PagingInfo { get; set; } } }
我們現在可以更新在ProductController(控制器)類里的List方法(Action)使用ProductsListViewModel類提供視圖與產品的細節上顯示的頁面和詳細的分頁.修改的ProductController(控制器)如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Concrete; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { public int PageSize = 4; //設置一頁顯示多少商品 private IProductRepository repository; public ProductController(IProductRepository productReposittory) { this.repository = productReposittory; } //返回一個視圖 public ViewResult List(int page = 1) { //return this.View(this.repository.Products.OrderBy(p => p.ProductID) // .Skip((page - 1) * PageSize) // .Take(PageSize)); ProductsListViewModel viewModel = new ProductsListViewModel { Products = this.repository.Products.OrderBy(h => h.ProductID) .Skip((page - 1) * PageSize) .Take(PageSize), PagingInfo = new PagingInfo { CurrentPage = page, ItemsPerPage = PageSize, TotalItems = this.repository.Products.Count() } }; return this.View(viewModel); } } }
這樣修改后視圖是預期返回Product對象的序列,所以我們需要修改前台List.cshtml,具體代碼如下:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Product List"; } @foreach (var p in Model.Products) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }
上面紅色加粗部分就是告訴Razor引擎我們已經在使用一個不同的類型,所以我們也需要更新數據源。這樣寫好,可以運行一下程序,如下圖9
圖9.這里可以看到分頁的標識已經顯示出來了。
那么這里為什么不直接使用一個GridView嗎? 因為我們用Asp.Net Mvc構建了一個穩固並且可維護的架構,這里面包含了分解關注點的思想。不像使用GridView控件,將UI跟數據訪問耦合在了一起,這樣做非常快而且方便,但從長遠看,這只是圖一時的方便給后期的維護留下一個大坑。
有這么一個不太爽的地方,不知道朋友注意到了沒有,我們現在運行起來的Web項目似乎的URL看着不太友好,如下圖10.
這樣的URL換做誰都不想讓他存在,我們是不是可以弄個更加漂亮的URL地址出來呢!比如這種:Http://localhst:XXXX/page2或者Http://localhst:XXXX/page_2....這樣看起來或許自己或許舒服點,在我們的MVC項目里,我們可以給他添加一條路由來改變它,因為MVC有Asp.net路由特性。我們在web項目的Global.asax文件里給他添加一條新的路由,具體如下:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); //添加一條路由 routes.MapRoute( null, "Page{page}", new { controller = "Product", action = "List" } ); routes.MapRoute( "Default", // 路由名稱 "{controller}/{action}/{id}", // 帶有參數的 URL new { controller = "Product", action = "List", id = UrlParameter.Optional } // 參數默認值 ); }
上面加粗部分就是我們添加新的路由,現在運行項目我們可以看到如下如11的URL。
圖11.和圖10的相比起來這個URL是不是看起來就爽多了。
因為我們要做一個簡單的項目,一直用這樣沒有樣式的頁面不太好了吧!那就給我們項目加入一點簡單的樣式(Css),首先來修改一下我們的模板(_Layout.cshtml)頁面,具體修改如下:
<!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script> </head> <body> <div id="header"> <div class="title">SPOPTS STORE</div> </div> <div id="categories">Will put something useful here later</div> <div id="content"> @RenderBody() </div> </body> </html>
然后添加樣式,我們在Mvc Web項目的Conten文件夾下的Site.css(如下圖)樣式表里添加樣式,原來生成的樣式不要刪除,我只需要在補充的添加一些樣式到樣式表里面。添加的樣式如下:
(樣式文件在項目的位置)
BODY { font-family: Cambria, Georgia, "Times New Roman"; margin: 0; } DIV#header DIV.title, DIV.item H3, DIV.item H4, DIV.pager A { font: bold 1em "Arial Narrow", "Franklin Gothic Medium", Arial; } DIV#header { background-color: #444; border-bottom: 2px solid #111; color: White; } DIV#header DIV.title { font-size: 2em; padding: .6em; } DIV#content { border-left: 2px solid gray; margin-left: 9em; padding: 1em; } DIV#categories { float: left; width: 8em; padding: .3em; } DIV.item { border-top: 1px dotted gray; padding-top: .7em; margin-bottom: .7em; } DIV.item:first-child { border-top:none; padding-top: 0; } DIV.item H3 { font-size: 1.3em; margin: 0 0 .25em 0; } DIV.item H4 { font-size: 1.1em; margin:.4em 0 0 0; } DIV.pager { text-align:right; border-top: 2px solid silver; padding: .5em 0 0 0; margin-top: 1em; } DIV.pager A { font-size: 1.1em; color: #666; text-decoration: none; padding: 0 .4em 0 .4em; } DIV.pager A:hover { background-color: Silver; } DIV.pager A.selected { background-color: #353535; color: White; } DIV#categories A { font: bold 1.1em "Arial Narrow","Franklin Gothic Medium",Arial; display: block; text-decoration: none; padding: .6em; color: Black; border-bottom: 1px solid silver; } DIV#categories A.selected { background-color: #666; color: White; } DIV#categories A:hover { background-color: #CCC; } DIV#categories A.selected:hover { background-color: #666; } FORM { margin: 0; padding: 0; } DIV.item FORM { float:right; } DIV.item INPUT { color:White; background-color: #333; border: 1px solid black; cursor:pointer; } H2 { margin-top: 0.3em } TFOOT TD { border-top: 1px dotted gray; font-weight: bold; } .actionButtons A, INPUT.actionButtons { font: .8em Arial; color: White; margin: .5em; text-decoration: none; padding: .15em 1.5em .2em 1.5em; background-color: #353535; border: 1px solid black; } DIV#cart { float:right; margin: .8em; color: Silver; background-color: #555; padding: .5em .5em .5em 1em; } DIV#cart A { text-decoration: none; padding: .4em 1em .4em 1em; line-height:2.1em; margin-left: .5em; background-color: #333; color:White; border: 1px solid black;}
OK,添加好樣式后,運行我們的程序如下圖12.
圖12.這樣看起來雖然有點丑了,但是比起之前的頁面要好多了。
我們展示數據的部分完全可以使用"局部視圖"來顯示的,我們可以創建一個局部視圖頁面來嵌套在List展示頁面,這樣有什么好處呢!我們知道,作為Action的響應,最常見的做法是Return View();也就是說,返回一個視圖。但是如果我們某的操作只是要返回頁面的一部分(局部的部分【局部視圖】),典型的情況就是,在頁面上實現局部的刷新功能。
那么我們來創建一個局部視圖,在我們Mvc Web項目的Views文件夾里的Shared文件夾里創建一個視圖,如下圖13.
圖13.我們新建的局部試圖也是強類型。創建好后我們的ProductSummary.cshtml頁面的代碼如下:
@model SportsStore.Domain.Entities.Product @{ ViewBag.Title = "ProductSummary"; } <div class="item"> <h3>@Model.Name</h3> @Model.Description <h4>@Model.Price.ToString("C")</h4> </div>
OK,我們的局部視圖創建完成后,我們在List.cshtml頁面要用它,在程序運行時要從局部試圖頁面展示出數據到list頁面上,那么List.cshtml的代碼修改如下:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Product List"; } @foreach (var Pro in Model.Products) { Html.RenderPartial("ProductSummary", Pro); } <div class="pager"> @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { Page = x })) </div>
上面代碼紅色加粗的部分就是引入局部試圖到List頁面。Html.RenderPartial方法需要傳入對象參數。
添加好局部試圖,運行我們的程序如下圖12.
圖12.好了,今天的項目就寫到這里,后續繼續,因為這話的東西配置方面的東西比較多,比較雜,想學習的同學還是有這個耐心看完的,文章寫的有些倉促,所以肯定有描述錯誤的地方,還請路過的前輩,朋友給點指點。共同進步,謝謝!