.NET技术支持者

  博客园 :: 首页 :: :: 联系 :: 订阅 :: 管理 ::

利用同一 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 Http­Handlers 添加最少的内容:

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

 

 

posted on 2013-06-20 14:00  LDAR泄漏检测与修复  阅读(1813)  评论(5)    收藏  举报