2001 年,Microsoft 在推出 Microsoft .NET Framework 的同时推出了一项称为 ASP.NET 的新技术,Web 开发人员广泛采用该技术使用基于窗体的框架构建网站。 这一称为 Web 窗体的框架经受住了八年的时间考验,历经不断完善和更改来支持不断发展的 Web 环境。 最初,创建 Web 应用程序只需做出简单的选择,“新建项目”对话框提供四个 ASP.NET 选项,如图 1 所示。 我们大多数人忽略了 ASP.NET 移动 Web 站点和 Web 控件库项目,只构建 ASP.NET Web 应用程序项目。 如果需要 Web 服务,则通过 .asmx 文件向现有的 Web 站点添加一个基于 SOAP 的服务。
 
 
          
            图 1 最初在 Visual C# 中新建 ASP.NET 项目的选择
          
        
2009 年初,ASP.NET 的状况发生了巨大的变化,推出了模型 - 视图 - 控制器 (MVC)。 由于不再需要处理视图状态、页面事件生命周期或者回发事件,开发人员纷纷采用这一新框架。 我便是其中之一,被这项更容易测试的 Web 技术的潜力深深吸引。 为了将应用程序切换到 MVC,我们不得不设法就预算说服管理人员和成本中心。为了在现有的 Web 窗体应用程序中呈现 MVC 内容,许多开发人员都经历过这样的过程。 几年来,MVC 运行非常出色,Web 也有所发展。 ASP.NET 需要再次发展。
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:
- <%@ Page Language="C#" AutoEventWireup="true"
 - MasterPageFile="~/Views/Shared/Site.Master"
 - Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
 - <%@ Import Namespace="System.Web.Mvc" %>
 - <asp:Content id="bodyContent" runat="server"
 - ContentPlaceHolderID="body">
 - <% Html.RenderPartial((string)ViewBag._ViewName); %>
 - </asp:Content>
 
为了让我的 MVC 控制器使用此视图,并使其基于 Razor 的视图得到执行,我需要对每个控制器进行扩展,以正确路由视图内容。 这通过一个扩展方法来实现,该方法通过 RazorView.aspx 对模型、ViewData 和 TempData 重新进行路由,如图 6 所示。
图 6 通过 Web 窗体母版页重新路由 MVC 视图的 RazorView 扩展方法
- public static ViewResult RazorView(this Controller controller,
 - string viewName = null, object model = null)
 - {
 - if (model != null)
 - controller.ViewData.Model = model;
 - controller.ViewBag._ViewName = !string.IsNullOrEmpty(viewName)
 - ?
 - viewName
 - : controller.RouteData.GetRequiredString("action");
 - return new ViewResult
 - {
 - ViewName = "RazorView",
 - ViewData = controller.ViewData,
 - TempData = controller.TempData
 - };
 - }
 
构建此方法后,我就可以通过母版页轻松路由所有 MVC 操作。 下一个步骤是设置 ProductsController 以便能够创建产品。
MVC 和创建产品屏幕
此解决方案的 MVC 部分遵循相当标准的 MVC 方法。 在我的项目的“模型”文件夹,我定义了一个简单的模型对象,称为 BoardGame,如图 7 所示。
图 7 BoardGame 对象
- public class BoardGame
 - {
 - public int Id { get; set; }
 - public string Name { get; set; }
 - [DisplayFormat(DataFormatString="$0.00")]
 - public decimal Price { get; set; }
 - [Display(Name="Number of items in stock"), Range(0,10000)]
 - public int NumInStock { get; set; }
 - }
 
接下来,我使用 Visual Studio 中标准的 MVC 工具创建一个空的 ProductController。 我将添加“视图”|“产品”文件夹,然后右键单击“产品”文件夹,再从“添加”菜单选择“视图”。 此视图将支持新 BoardGame 的创建,所以我将使用图 8 中所示的选项创建。
 
 
          
            图 8 创建“新建”视图
          
        
由于使用了 MVC 工具和模板,我不需要进行任何更改。 创建的视图带有标签和验证,并可以使用我的母版页。 图 9 显示如何在 ProductController 中定义“新建”操作。
图 9 通过 RazorView 的 ProductController 路由
- public ActionResult New()
 - {
 - return this.RazorView();
 - }
 - [HttpPost]
 - public ActionResult New(BoardGame newGame)
 - {
 - if (!ModelState.IsValid)
 - {
 - return this.RazorView();
 - }
 - newGame.Id = _Products.Count + 1;
 - _Products.Add(newGame);
 - return Redirect("~/Product/Search");
 - }
 
MVC 开发人员应熟悉此语法,唯一的变化是返回一个 RazorView,而不是视图。 _Products 对象是一个此控制器中所定义的虚产品的静态只读集合,而不是使用此示例中的数据库:
- public static readonly List<BoardGame> _Products =
 - new List<BoardGame>()
 - {
 - new BoardGame() {Id=1, Name="Chess", Price=9.99M},
 - new BoardGame() {Id=2, Name="Checkers", Price=7.99M},
 - new BoardGame() {Id=3, Name="Battleship", Price=8.99M},
 - new BoardGame() {Id=4, Name="Backgammon", Price= 12.99M}
 - };
 
配置基于 Web 窗体的搜索页
我希望用户访问产品搜索页面的 URL 有别于 Web 窗体的 URL,能够便于用户搜索。 随着 ASP.NET 2012.2 的发布,现在可以轻松完成这一配置。 只需打开 App_Start/ RouteConfig.cs 文件,并调用 EnableFriendlyUrls 以启动此功能:
- public static void RegisterRoutes(
 - RouteCollection routes)
 - {
 - routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 - routes.EnableFriendlyUrls();
 - routes.MapRoute(
 - name: "Default",
 - url: "{controller}/{action}/{id}",
 - defaults: new { controller = "Home", action =
 - "Index", id = UrlParameter.Optional }
 - );
 - }
 
添加这一行后,ASP.NET 将把 /Product/Search 请求路由到位于 /Product/Search.aspx 的物理文件
接下来,我要配置一个显示目前产品及其库存水平的网格的搜索页面。 我将在我的项目中创建一个“产品”文件夹并向其添加一个名为 Search.aspx 的新 Web 窗体。 在此文件中,我将去掉除 @Page 指令之外的所有标记,并将 MasterPageFile 设置为前面定义的 Site.Master 文件。 为了显示我的结果,我将选择 Telerik RadGrid,这样我就可以快速配置并显示结果数据:
- <%@ Page Language="C#" AutoEventWireup="true"
 - CodeBehind="Search.aspx.cs"
 - Inherits="MvcApplication1.Product.Search"
 - MasterPageFile="~/Views/Shared/Site.Master" %>
 - <asp:Content runat="server" id="main" ContentPlaceHolderID="body">
 - <telerik:RadGrid ID="searchProducts" runat="server" width="500"
 - AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
 - AllowSorting="True">
 
网格将自动生成绑定到其上的列,并提供排序和筛选功能。 不过,我希望提高这一过程的动态性。 我想在客户端实现数据的交付和管理。 在此模型中,数据可以在 Web 窗体中无服务器端代码的情况下被发送并绑定。 为此,我将添加一个负责交付并执行数据操作的 Web API。
向组合中添加 Web API
使用标准的“项目” | “新增”菜单,我将一个名为 ProductController 的 Web API 控制器添加到我的项目中名为“api”的文件夹。 这有助于我清楚了解 MVC 控制器和 API 控制器之间的差别。 此 API 将完成一项工作 — 以 JSON 格式交付网格数据并支持 OData 查询。 要在 Web API 中完成这一点,我将编写一个 Get 方法并为其添加 Queryable 属性:
- [Queryable]
 - public IQueryable<dynamic> Get(ODataQueryOptions options)
 - {
 - return Controllers.ProductController._Products.Select(b => new
 - {
 - Id = b.Id,
 - Name = b.Name,
 - NumInStock = b.NumInStock,
 - Price = b.Price.ToString("$0.00")
 - }).AsQueryable();
 - }
 
此代码经过少许格式化处理之后,返回静态列表中的 BoardGame 对象集合。 通过使用 [Queryable] 修饰该方法并返回可查询的集合,Web API 框架会自动接手处理 OData 筛选和排序命令。 该方法还需要使用输入参数 ODataQueryOptions 进行配置,以便处理网格提交的筛选数据。
如果要在 Search.aspx 上配置网格以使用此新 API,我需要向页面标记添加一些客户端设置。 在此网格控件中,我使用 ClientSettings 元素和 DataBinding 设置定义客户端数据绑定。 DataBinding 设置列出了 API 的位置、响应格式类型和要查询的控制器名称,以及 OData 查询格式。 通过这些设置和要在网格中显示的列的定义,我可以运行该项目,并看到绑定到 _Products 虚数据列表中数据的网格,如图 10 所示。
图 10 网格的完整格式源
- <telerik:RadGrid ID="searchProducts" runat="server" width="500"
 - AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
 - AllowSorting="True" AutoGenerateColumns="false"
 - >
 - <ClientSettings AllowColumnsReorder="True"
 - ReorderColumnsOnClient="True"
 - ClientEvents-OnGridCreated="GridCreated">
 - <Scrolling AllowScroll="True" UseStaticHeaders="True"></Scrolling>
 - <DataBinding Location="/api" ResponseType="JSON">
 - <DataService TableName="Product" Type="OData" />
 - </DataBinding>
 - </ClientSettings>
 - <MasterTableView ClientDataKeyNames="Id" DataKeyNames="Id">
 - <Columns>
 - <telerik:GridBoundColumn DataField="Id" HeaderStyle-Width="0"
 - ItemStyle-Width="0"></telerik:GridBoundColumn>
 - <telerik:GridBoundColumn DataField="Name" HeaderText="Name"
 - HeaderStyle-Width="150" ItemStyle-Width="150">
 - </telerik:GridBoundColumn>
 - <telerik:GridBoundColumn ItemStyle-CssClass="gridPrice"
 - DataField="Price"
 - HeaderText="Price" ItemStyle-HorizontalAlign="Right">
 - </telerik:GridBoundColumn>
 - <telerik:GridBoundColumn DataField="NumInStock"
 - ItemStyle-CssClass="numInStock"
 - HeaderText="# in Stock"></telerik:GridBoundColumn>
 - </Columns>
 - </MasterTableView>
 - </telerik:RadGrid>
 
使用实时数据激活网格
最后一个步骤是随着产品出货和进货实时显示库存水平变化的功能。 我将添加一个 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 服务器端组件
- public class StockHub : Hub
 - {
 - public static readonly Timer _Timer = new Timer();
 - private static readonly Random _Rdm = new Random();
 - static StockHub()
 - {
 - _Timer.Interval = 2000;
 - _Timer.Elapsed += _Timer_Elapsed;
 - _Timer.Start();
 - }
 - static void _Timer_Elapsed(object sender, ElapsedEventArgs e)
 - {
 - var products = ProductController._Products;
 - var p = products.Skip(_Rdm.Next(0, products.Count())).First();
 - var newStockLevel = p.NumInStock +
 - _Rdm.Next(-1 * p.NumInStock, 100);
 - p.NumInStock = newStockLevel;
 - var hub = GlobalHost.ConnectionManager.GetHubContext<StockHub>();
 - hub.Clients.All.setNewStockLevel(p.Id, newStockLevel);
 - }
 - }
 
为使此 Hub 的数据可从服务器访问,我需要向 RouteConfig 添加一行,表明 Hub 的存在。 通过在 RouteConfig 的 RegisterRoutes 方法中调用 routes.MapHubs,我完成了 SignalR 服务器端的配置。
接下来,网格需要侦听这些来自服务器的事件。 为此,我需要添加一些从 NuGet 安装的 SignalR 客户端库和 MapHubs 命令生成的代码的 JavaScript 引用。 SignalR 服务使用图 12 中显示的代码连接并公开客户端上的 setNewStockLevel 方法。
图 12 用于激活网格的 SignalR 客户端代码
- <script src="/Scripts/jquery.signalR-1.0.0-rc2.min.js"></script>
 - <script src="/signalr/hubs"></script>
 - <script type="text/javascript">
 - var grid;
 - $().ready(function() {
 - var stockWatcher = $.connection.stockHub;
 - stockWatcher.client.setNewStockLevel = function(id, newValue) {
 - var row = GetRow(id);
 - var orgColor = row.css("background-color");
 - row.find(".
 - numInStock").animate({
 - backgroundColor: "#FFEFD5"
 - }, 1000, "swing", function () {
 - row.find(".
 - numInStock").html(newValue).animate({
 - backgroundColor: orgColor
 - }, 1000)
 - });
 - };
 - $.connection.hub.start();
 - })
 - </script>
 
在 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 框架。 而是要各取所需。
                    
                
                
            
        
浙公网安备 33010602011771号