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 框架。 而是要各取所需。