2012 年,Microsoft 推出了兩個添加到 ASP.NET 工具包的新框架:Web API 和 SignalR。 這兩個框架為開發環境帶來獨特的開發方式,每個框架都有自身的獨特之處:
- Web API 為開發人員提供了類似 MVC 的體驗,以交付針對機器解釋的內容。 沒有用戶界面,並且事務以 RESTful 的方式出現。 內容類型經過協商后,基於提交到 Web API 端點的 HTTP 標頭,Web API 就可以將內容自動格式化為 JSON 或 XML。
- SignalR 是來自 Microsoft 的新型“實時 Web”交付模型。 此技術打開了客戶端 - 服務器通信通道,支持進行從服務器到客戶端的即時豐富通信。 由於是通過服務器調用客戶端來實現內容交互,SignalR 中的內容交付模型顛覆了我們的正常預期。
Web 窗體和 MVC 之間以及 Web API 和 MVC 之間的利弊權衡如圖 2 所示。
圖 2 每個 ASP.NET 組件框架的優點
| 框架 |
效率 |
Control |
UI |
實時 |
| 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 內容。 這不是一個簡單的問題,但它可以解決。
彌合鴻溝 — 將 MVC 配置為使用 Web 窗體母版頁
像大多數人一樣,我喜歡讓事情變得簡單。 我來分享一下我為這個介於 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 部分遵循相當標准的 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.
配置基於 Web 窗體的搜索頁
我希望用戶訪問產品搜索頁面的 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。
向組合中添加 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 框架。 而是要各取所需。
