海东的技术资料

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

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 = nullobject 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  高海东  阅读(1688)  评论(5编辑  收藏  举报