2012 年,Microsoft 推出了兩個添加到 ASP.NET 工具包的新框架:Web API 和 SignalR。 這兩個框架爲開發環境帶來獨特的開發方式,每一個框架都有自身的獨特之處:javascript
Web 窗體和 MVC 之間以及 Web API 和 MVC 之間的利弊權衡如圖 2 所示。css
圖 2 每一個 ASP.NET 組件框架的優勢html
框架java |
效率jquery |
Control數據庫 |
UIapi |
實時服務器 |
Web 表單網絡 |
•架構 |
|
• |
|
MVC |
|
• |
• |
|
Web API |
• |
• |
|
|
SignalR |
|
|
|
• |
工做效率與容許您快速開發和交付解決方案的功能相關。 控制是可影響經過網絡向鏈接用戶傳輸的比特的程度。 UI 指示是否可使用該框架來交付完整的 UI。 最後,「實時」代表框架可以在多大程度上及時顯示即時更新的內容。
如今,在 2013 年,當我打開 Visual Studio 並試圖啓動一個 ASP.NET 項目時,我看到的是如圖 3 和圖 4 所示的對話框。
圖 3 Visual Studio 2012 中的新建 Web 項目
圖 4 Visual Studio 2012 中的新建項目模板對話框
在這些窗口中有一些棘手的問題。 我應從什麼類型的項目開始呢? 我應使用什麼模板才能最快得到解決方案呢? 若是我想要添加每一個模板的一些組件,將會怎樣? 我能夠構建一個帶有一些服務器控件和一個 Web API 的移動應用程序嗎?
我只能選擇一種方法嗎? 簡短的答案是否認的,您並不是只能選擇其中一種框架來構建 Web 應用程序。 如今已有一些技術容許您將 Web 窗體和 MVC 結合在一塊兒使用。與顯示的對話框窗口不一樣,Web API 和 SignalR 能夠做爲功能輕鬆添加到 Web 應用程序中。 請記住,全部 ASP.NET 內容都是經過一系列 HttpHandlers 和 HttpModules 呈現的。 只要引用了正確的處理程序和模塊,就可使用任何一種框架構來建解決方案。
這是「同一 ASP.NET」概念的核心:不要只選擇這些框架中的一個,應使用最符合您的需求的部分構建解決方案。 您有不少選擇,不要侷限於其中的一種。
咱們來具體看看這是怎麼實現的。爲此,我將建立一個小型的 Web 應用程序,其中包含統一佈局、一個搜索屏幕和一個產品列表的建立屏幕。 搜索屏幕將由 Web 窗體和 Web API 支持,並顯示來自 SignalR 的實時更新。 建立屏幕將由 MVC 模板自動生成。 經過使用第三方控件庫和麪向 ASP.NET AJAX 的 Telerik RadControls,我還將確保 Web 窗體具備精美的外觀。 這些控件的試用版可從 bit.ly/15o2Oab 得到。
我只須要使用圖 3 中所示的對話框建立一個項目就能夠開始了。 雖然我能夠選擇一個空的或 Web 窗體應用程序,能夠選擇的最完備解決方案則是 MVC 應用程序。 以 MVC 項目開始是很好的選擇,由於您從 Visual Studio 得到了全部的工具,可幫助您完成配置模型、視圖和控制器的過程,並可以將 Web 窗體對象添加到項目文件結構中的任何位置。 經過更改 .csproj 文件中的一些 XML 內容,可將 MVC 工具添加回現有 Web 應用程序。 此過程可經過安裝名爲 AddMvc3ToWebForms 的 NuGet 包自動完成。
若要配置在這個項目中使用的 Telerik 控件,我須要在 Web.config 中進行一些更改,以添加一般會在標準 Telerik RadControls 項目中配置的 HttpHandlers 和 HttpModules。 首先,我將添加幾行來定義 Telerik AJAX 控件 UI 皮膚:
<add key="Telerik.Skin" value="WebBlue" />
</appSettings>
接下來,添加 Telerik 標籤前綴:
<add tagPrefix="telerik" namespace="Telerik.Web.UI" assembly="Telerik.Web.UI" />
</controls>
我將爲 Telerik 控件的 Web.config HttpHandlers 添加最少的內容:
<add path="Telerik.Web.UI.WebResource.axd" type="Telerik.Web.UI.WebResource"
verb="*" validate="false" />
</httpHandlers>
最後,我將添加到 Telerik 控件的 Web.config Handlers:
<system.WebServer>
<validation validateIntegratedModeConfiguration="false" />
<handlers>
<remove name="Telerik_Web_UI_WebResource_axd" />
<add name="Telerik_Web_UI_WebResource_axd"
path="Telerik.Web.UI.WebResource.axd"
type="Telerik.Web.UI.WebResource" verb="*" preCondition="integratedMode" />
如今,我要爲這個項目建立一個佈局頁,因此我將在「視圖」 | 「共享」文件夾中建立一個 Web 窗體 site.master 頁。 對於此站點佈局,我要將標準的徽標和菜單添加到全部頁。 我將經過簡單地將圖像拖到佈局上來添加一個徽標圖像。 接下來,爲了將一個主要的級聯菜單添加到佈局,我將從控件工具箱把 RadMenu 拖到圖像正下方的設計器上。 從設計器圖面,經過右鍵單擊菜單控件並選擇「編輯項目」以獲得圖 5 所示的窗口,我能夠快速構建菜單。
圖 5 Telerik RadMenu 配置窗口
我要重點關注的兩個菜單項位於「產品」下:「搜索」和「新建」。 對於每一個項目,我已對 NavigateUrl 屬性和文本進行以下設置:
<telerik:RadMenuItem Text="Products">
<Items>
<telerik:RadMenuItem Text="Search" NavigateUrl="~/Product/Search" />
<telerik:RadMenuItem Text="New" NavigateUrl="~/Product/New" />
</Items>
</telerik:RadMenuItem>
菜單配置好之後,我如今遇到了新問題:我使用 Web 窗體定義佈局,但須要承載 MVC 內容。 這不是一個簡單的問題,但它能夠解決。
像大多數人同樣,我喜歡讓事情變得簡單。 我來分享一下我爲這個介於 Web 窗體和 MVC 之間的項目定義的佈局。 Matt Hawley 設計了一項技術(有完善的文檔),演示瞭如何結合使用 Web 窗體母版頁和基於 MVC Razor 的視圖 (bit.ly/ehVY3H)。 我將在這個項目中使用該技術。 爲了建立這樣一個橋樑,我將配置一個引用母版頁的簡單 Web 窗體視圖,稱爲 RazorView.aspx:
1.
2. <%@ Page Language="C#" AutoEventWireup="true"
3. MasterPageFile="~/Views/Shared/Site.Master"
4. Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
5. <%@ Import Namespace="System.Web.Mvc" %>
6. <asp:Content id="bodyContent" runat="server"
7. ContentPlaceHolderID="body">
8. <% Html.RenderPartial((string)ViewBag._ViewName); %>
9. </asp:Content>
10.
爲了讓個人 MVC 控制器使用此視圖,並使其基於 Razor 的視圖獲得執行,我須要對每一個控制器進行擴展,以正確路由視圖內容。 這經過一個擴展方法來實現,該方法經過 RazorView.aspx 對模型、ViewData 和 TempData 從新進行路由,如圖 6 所示。
圖 6 經過 Web 窗體母版頁從新路由 MVC 視圖的 RazorView 擴展方法
1.
2. public static ViewResult RazorView(this Controller controller,
3. string viewName = null, object model = null)
4. {
5. if (model != null)
6. controller.ViewData.Model = model;
7. controller.ViewBag._ViewName = !string.IsNullOrEmpty(viewName)
8. ?
9. viewName
10. : controller.RouteData.GetRequiredString("action");
11. return new ViewResult
12. {
13. ViewName = "RazorView",
14. ViewData = controller.ViewData,
15. TempData = controller.TempData
16. };
17. }
18.
構建此方法後,我就能夠經過母版頁輕鬆路由全部 MVC 操做。 下一個步驟是設置 ProductsController 以便可以建立產品。
此解決方案的 MVC 部分遵循至關標準的 MVC 方法。 在個人項目的「模型」文件夾,我定義了一個簡單的模型對象,稱爲 BoardGame,如圖 7 所示。
圖 7 BoardGame 對象
1.
2. public class BoardGame
3. {
4. public int Id { get; set; }
5. public string Name { get; set; }
6. [DisplayFormat(DataFormatString="$0.00")]
7. public decimal Price { get; set; }
8. [Display(Name="Number of items in stock"), Range(0,10000)]
9. public int NumInStock { get; set; }
10. }
11.
接下來,我使用 Visual Studio 中標準的 MVC 工具建立一個空的 ProductController。 我將添加「視圖」|「產品」文件夾,而後右鍵單擊「產品」文件夾,再從「添加」菜單選擇「視圖」。 此視圖將支持新 BoardGame 的建立,因此我將使用圖 8 中所示的選項建立。
圖 8 建立「新建」視圖
因爲使用了 MVC 工具和模板,我不須要進行任何更改。 建立的視圖帶有標籤和驗證,並可使用個人母版頁。 圖 9 顯示如何在 ProductController 中定義「新建」操做。
圖 9 經過 RazorView 的 ProductController 路由
1.
2. public ActionResult New()
3. {
4. return this.RazorView();
5. }
6. [HttpPost]
7. public ActionResult New(BoardGame newGame)
8. {
9. if (!ModelState.IsValid)
10. {
11. return this.RazorView();
12. }
13. newGame.Id = _Products.Count + 1;
14. _Products.Add(newGame);
15. return Redirect("~/Product/Search");
16. }
17.
MVC 開發人員應熟悉此語法,惟一的變化是返回一個 RazorView,而不是視圖。 _Products 對象是一個此控制器中所定義的虛產品的靜態只讀集合,而不是使用此示例中的數據庫:
1.
2. public static readonly List<BoardGame> _Products =
3. new List<BoardGame>()
4. {
5. new BoardGame() {Id=1, Name="Chess", Price=9.99M},
6. new BoardGame() {Id=2, Name="Checkers", Price=7.99M},
7. new BoardGame() {Id=3, Name="Battleship", Price=8.99M},
8. new BoardGame() {Id=4, Name="Backgammon", Price= 12.99M}
9. };
10.
我但願用戶訪問產品搜索頁面的 URL 有別於 Web 窗體的 URL,可以便於用戶搜索。 隨着 ASP.NET 2012.2 的發佈,如今能夠輕鬆完成這一配置。 只需打開 App_Start/ RouteConfig.cs 文件,並調用 EnableFriendlyUrls 以啓動此功能:
1.
2. public static void RegisterRoutes(
3. RouteCollection routes)
4. {
5. routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
6. routes.EnableFriendlyUrls();
7. routes.MapRoute(
8. name: "Default",
9. url: "{controller}/{action}/{id}",
10. defaults: new { controller = "Home", action =
11. "Index", id = UrlParameter.Optional }
12. );
13. }
14.
添加這一行後,ASP.NET 將把 /Product/Search 請求路由到位於 /Product/Search.aspx 的物理文件
接下來,我要配置一個顯示目前產品及其庫存水平的網格的搜索頁面。 我將在個人項目中建立一個「產品」文件夾並向其添加一個名爲 Search.aspx 的新 Web 窗體。 在此文件中,我將去掉除 @Page 指令以外的全部標記,並將 MasterPageFile 設置爲前面定義的 Site.Master 文件。 爲了顯示個人結果,我將選擇 Telerik RadGrid,這樣我就能夠快速配置並顯示結果數據:
1.
2. <%@ Page Language="C#" AutoEventWireup="true"
3. CodeBehind="Search.aspx.cs"
4. Inherits="MvcApplication1.Product.Search"
5. MasterPageFile="~/Views/Shared/Site.Master" %>
6. <asp:Content runat="server" id="main" ContentPlaceHolderID="body">
7. <telerik:RadGrid ID="searchProducts" runat="server" width="500"
8. AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
9. AllowSorting="True">
10.
網格將自動生成綁定到其上的列,並提供排序和篩選功能。 不過,我但願提升這一過程的動態性。 我想在客戶端實現數據的交付和管理。 在此模型中,數據能夠在 Web 窗體中無服務器端代碼的狀況下被髮送並綁定。 爲此,我將添加一個負責交付並執行數據操做的 Web API。
使用標準的「項目」 | 「新增」菜單,我將一個名爲 ProductController 的 Web API 控制器添加到個人項目中名爲「api」的文件夾。 這有助於我清楚瞭解 MVC 控制器和 API 控制器之間的差異。 此 API 將完成一項工做 — 以 JSON 格式交付網格數據並支持 OData 查詢。 要在 Web API 中完成這一點,我將編寫一個 Get 方法併爲其添加 Queryable 屬性:
1.
2. [Queryable]
3. public IQueryable<dynamic> Get(ODataQueryOptions options)
4. {
5. return Controllers.ProductController._Products.Select(b => new
6. {
7. Id = b.Id,
8. Name = b.Name,
9. NumInStock = b.NumInStock,
10. Price = b.Price.ToString("$0.00")
11. }).AsQueryable();
12. }
13.
此代碼通過少量格式化處理以後,返回靜態列表中的 BoardGame 對象集合。 經過使用 [Queryable] 修飾該方法並返回可查詢的集合,Web API 框架會自動接手處理 OData 篩選和排序命令。 該方法還須要使用輸入參數 ODataQueryOptions 進行配置,以便處理網格提交的篩選數據。
若是要在 Search.aspx 上配置網格以使用此新 API,我須要向頁面標記添加一些客戶端設置。 在此網格控件中,我使用 ClientSettings 元素和 DataBinding 設置定義客戶端數據綁定。 DataBinding 設置列出了 API 的位置、響應格式類型和要查詢的控制器名稱,以及 OData 查詢格式。 經過這些設置和要在網格中顯示的列的定義,我能夠運行該項目,並看到綁定到 _Products 虛數據列表中數據的網格,如圖 10 所示。
圖 10 網格的完整格式源
1.
2. <telerik:RadGrid ID="searchProducts" runat="server" width="500"
3. AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
4. AllowSorting="True" AutoGenerateColumns="false"
5. >
6. <ClientSettings AllowColumnsReorder="True"
7. ReorderColumnsOnClient="True"
8. ClientEvents-OnGridCreated="GridCreated">
9. <Scrolling AllowScroll="True" UseStaticHeaders="True"></Scrolling>
10. <DataBinding Location="/api" ResponseType="JSON">
11. <DataService TableName="Product" Type="OData" />
12. </DataBinding>
13. </ClientSettings>
14. <MasterTableView ClientDataKeyNames="Id" DataKeyNames="Id">
15. <Columns>
16. <telerik:GridBoundColumn DataField="Id" HeaderStyle-Width="0"
17. ItemStyle-Width="0"></telerik:GridBoundColumn>
18. <telerik:GridBoundColumn DataField="Name" HeaderText="Name"
19. HeaderStyle-Width="150" ItemStyle-Width="150">
20. </telerik:GridBoundColumn>
21. <telerik:GridBoundColumn ItemStyle-CssClass="gridPrice"
22. DataField="Price"
23. HeaderText="Price" ItemStyle-HorizontalAlign="Right">
24. </telerik:GridBoundColumn>
25. <telerik:GridBoundColumn DataField="NumInStock"
26. ItemStyle-CssClass="numInStock"
27. HeaderText="# in Stock"></telerik:GridBoundColumn>
28. </Columns>
29. </MasterTableView>
30. </telerik:RadGrid>
31.
最後一個步驟是隨着產品出貨和進貨實時顯示庫存水平變化的功能。 我將添加一個 SignalR hub 以傳輸更新信息並在搜索網格上顯示新值。 要將 SignalR 添加到個人項目,我須要發出如下兩個 NuGet 命令:
Install-Package -pre Microsoft.AspNet.SignalR.SystemWeb
Install-Package -pre Microsoft.AspNet.SignalR.JS
這些命令將在 IIS Web 服務器中安裝要承載的 ASP.NET 服務器組件,併爲 Web 窗體啓動客戶端 JavaScript 庫。
SignalR 服務器端組件被稱爲 Hub,我將定義我本身的 Hub,方法是在個人 Web 項目中的 Hubs 文件夾添加一個名爲 StockHub 的類。 StockHub 需從 Microsoft.AspNet.SignalR.Hub 類繼承而得。 我定義了一個靜態的 System.Timers.Timer,使應用程序可以模擬庫存水平的變化。 模擬方式是每隔 2 秒(觸發定時器 Elapsed 事件處理程序),我會隨機設置一個隨機選擇產品的庫存水平。 一旦設置了產品庫存水平,經過在客戶端執行一個名爲 setNewStockLevel 的方法,我將通知全部鏈接的客戶端,如圖 11 中所示。
圖 11 SignalR Hub 服務器端組件
1.
2. public class StockHub : Hub
3. {
4. public static readonly Timer _Timer = new Timer();
5. private static readonly Random _Rdm = new Random();
6. static StockHub()
7. {
8. _Timer.Interval = 2000;
9. _Timer.Elapsed += _Timer_Elapsed;
10. _Timer.Start();
11. }
12. static void _Timer_Elapsed(object sender, ElapsedEventArgs e)
13. {
14. var products = ProductController._Products;
15. var p = products.Skip(_Rdm.Next(0, products.Count())).First();
16. var newStockLevel = p.NumInStock +
17. _Rdm.Next(-1 * p.NumInStock, 100);
18. p.NumInStock = newStockLevel;
19. var hub = GlobalHost.ConnectionManager.GetHubContext<StockHub>();
20. hub.Clients.All.setNewStockLevel(p.Id, newStockLevel);
21. }
22. }
23.
爲使此 Hub 的數據可從服務器訪問,我須要向 RouteConfig 添加一行,代表 Hub 的存在。 經過在 RouteConfig 的 RegisterRoutes 方法中調用 routes.MapHubs,我完成了 SignalR 服務器端的配置。
接下來,網格須要偵聽這些來自服務器的事件。 爲此,我須要添加一些從 NuGet 安裝的 SignalR 客戶端庫和 MapHubs 命令生成的代碼的 JavaScript 引用。 SignalR 服務使用圖 12 中顯示的代碼鏈接並公開客戶端上的 setNewStockLevel 方法。
圖 12 用於激活網格的 SignalR 客戶端代碼
1.
2. <script src="/Scripts/jquery.signalR-1.0.0-rc2.min.js"></script>
3. <script src="/signalr/hubs"></script>
4. <script type="text/javascript">
5. var grid;
6. $().ready(function() {
7. var stockWatcher = $.connection.stockHub;
8. stockWatcher.client.setNewStockLevel = function(id, newValue) {
9. var row = GetRow(id);
10. var orgColor = row.css("background-color");
11. row.find(".
12. numInStock").animate({
13. backgroundColor: "#FFEFD5"
14. }, 1000, "swing", function () {
15. row.find(".
16. numInStock").html(newValue).animate({
17. backgroundColor: orgColor
18. }, 1000)
19. });
20. };
21. $.connection.hub.start();
22. })
23. </script>
24.
在 jQuery 就緒事件處理程序中,我使用 $.connection.stockHub 語法創建了對 StockHub 的引用,名爲 stockWatcher。 而後爲 stockWatcher 的客戶端屬性定義了 setNewStockLevel 方法。 此方法使用其餘一些 JavaScript 幫助器方法遍歷網格,找到相應產品的行,並使用 jQuery UI 提供的絢麗動畫更改庫存水平,如圖 13 所示。
圖 13 由 Web API 生成並由 SignalR 維護的網格搜索界面
至此,我演示瞭如何構建一個 ASP.NET MVC 項目並添加 Web 窗體佈局、第三方 AJAX 控件及相應的 Web 窗體。 我使用 MVC 工具生成用戶界面,並使用 Web API 和 SignalR 激活內容。 此項目利用各組件的最佳功能,使用來自全部四個 ASP.NET 框架的功能,提供了一個一致的界面。 您也來試試吧。 對於您之後的項目,您將沒必要侷限於一種 ASP.NET 框架。 而是要各取所需。