ASP.NET MVC 5

单页应用程序的 .NET 开发人员入门

Long Le

下载代码示例

大多数 Microsoft .NET Framework 开发人员把大部分工作时间花在了服务器端,在构建 Web 应用程序时使用 C# 或 Visual Basic .NET 来编写代码。当然,也会使用 JavaScript 设计简单的东西,如模式窗口、验证、AJAX 调用等等。但是,JavaScript(大多数情况下用于编写客户端代码)已被人们用作实用语言工具,应用程序大多从服务器端推动构建而成。

最近,为了满足用户对流畅且响应迅速的用户体验的期望,Web 应用程序代码从服务器端向客户端(浏览器)迁移已成为一大趋势。在这种情况下,人们对于 JavaScript 最佳实践、体系结构、单元测试、可维护性以及近期大量不同类型的 JavaScript 库暴增等问题产生了巨大担忧,许多.NET 开发人员(特别是企业中的开发人员)都在应对由此带来的影响。导致代码向客户端迁移的趋势的部分原因是对单页应用程序 (SPA) 的使用不断增加。“SPA 开发是未来的发展方向”是一种极端保守的说法。Web 上一些最佳的应用程序之所以能够提供流畅的用户体验和响应能力,同时最大程度降低服务器负载(流量)以及与服务器之间的往返次数,都有赖于 SPA。

在本文中,我将帮助您打消从服务器端过渡到 SPA 领域时可能出现的顾虑。消除顾虑的最佳方法是将 JavaScript 作为第一流的语言采用,就像其他任何 .NET 语言(如 C#、Visual Basic .NET、Python 等)一样。

下面是在以 JavaScript 开发应用程序时,有时会忽略或忘记的一些基本 .NET 开发原则:

  • 您可在 .NET 中管理代码库,这是因为您负责决定类边界以及类在您的项目中实际处于何处。
  • 可将问题分开处理,这样就不会出现负责处理数百个不同事务、职责重叠的类。
  • 这样就有了可以重用的存储库、查询、实体(模型)和数据源。
  • 在命名类和文件时,可以加入一点自己的想法,以便让名称更有意义。
  • 充分利用设计模式、编码约定和组织。

因为本文面向 .NET 开发人员,向其介绍 SPA 世界,我会引入尽量少的框架,以构造一个具有可靠体系结构的可管理的 SPA。

分 7 个关键步骤创建 SPA

下面列出了 7 个关键步骤,将使用现成的 Visual Studio 2013 ASP.NET MVC 模板创建的一个全新 ASP.NET Web 应用程序转化为 SPA(引用了位于随附的代码下载中的相应项目文件)。

  1. 下载并安装 NuGet 程序包 RequireJS、RequireJS 文本插件和 Kendo UI Web。
  2. 添加一个配置模块 (Northwind.Web/Scripts/app/main.js)。
  3. 添加一个应用程序模块 (Northwind.Web/Scripts/app/app.js)。
  4. 添加一个路由器模块 (Northwind.Web/Scripts/app/router.js)。
  5. 添加名称均为 Spa 的一个操作和一个视图(Northwind.Web/Controllers/HomeController.cs 和 Northwind.Web/Views/Home/Spa.cshtml)。
  6. 修改 _ViewStart.cshtml 文件,让 MVC 能够加载视图而无需按默认设置使用 _Layout.cshtml 文件 (Northwind.Web/Views/_ViewStart.cshtml)。
  7. 更新布局导航(菜单)链接,以与新的 SPA 友好 URL (Northwind.Web/Views/Shared/_Layout.cshtml) 匹配。

这 7 个步骤执行完毕后,Web 应用程序项目结构看起来将类似于图 1 所示。

ASP.NET MVC Project Structure
图 1 ASP.NET MVC 项目结构

我将演示如何使用以下 JavaScript 库(可通过 NuGet 获取)在 ASP.NET MVC 中构建了不起的 SPA:

  • RequireJS (requirejs.org):这是一个 JavaScript 文件,也是一个模块加载程序。RequireJS 将提供 #include/import/require API,还具备使用依赖关系注入 (DI) 来加载嵌套依赖关系的能力。RequireJS 设计方法对 JavaScript 模块使用异步模块定义 (AMD) API,这将有助于将代码块封装为有用的单元。它还提供了一种直观的方式来引用其他代码单元(即模块)。RequireJS 模块还遵循模块模式 (bit.ly/18byc2Q)。简单实施这种模式就可使用 JavaScript 功能来实现封装。您稍后会看到我实际采用这种模式,我将在“define”或“require”函数中打包所有 JavaScript 模块。
  • 熟悉 DI 和控制反转 (IoC) 概念的人可以将其视为客户端的 DI 框架。如果现在还是难以理解,别担心,我马上会介绍一些编码示例,您会了解到其意义所在。
  • RequireJS (bit.ly/1cd8lTZ) 的文本插件:这将用于把 HTML 块(视图)远程加载到 SPA 中。
  • 实体框架 (bit.ly/1bKiZ9I):这个地址上的内容介绍得非常清楚,并且本文的重点是介绍 SPA,所以我不会过多地讨论实体框架。但是,如果您在这方面还是新手,那里也有大量文档供您查阅。
  • Kendo UI Web (bit.ly/t4VkVp):这是一个全面的 JavaScript/­HTML5 框架,其中包含 Web UI 小部件、数据源、模板、“模型-视图-视图模型 (MVVM)”模式、SPA、样式等等,以帮助您交付响应迅速且看起来很不错的自适应应用程序。

设置 SPA 基础结构

要显示如何设置 SPA 基础结构,我首先要解释一下如何创建 RequireJS(配置)模块 (Northwind.Web/Scripts/app/main.js)。此模块将是应用程序启动入口点。如果您已经创建了一个控制台应用程序,您可以将它视为 Program.cs 中的 Main 入口点。它基本包含了 SPA 启动时调用的第一个类和方法。main.js 文件基本上充当 SPA 的清单,它位于您将定义 SPA 中的一切内容及其依赖关系(如果有)所处的位置。RequireJS 配置的代码如图 2 所示。

图 2 RequireJS 配置

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router'
  },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
});
require([
  'app'
], function (app) {
  app.initialize();
});

图 2 中,路径属性包含所有模块所在位置以及模块名称的列表。Shim 是先前定义的模块的名称。shim 属性包括该模块可能具有的所有依赖关系。在此例中,您将加载一个名为 kendo 的模块,它依赖于名为 jQuery 的模块,因此如果一个模块需要 kendo 模块,则会先加载 jQuery,因为 jQuery 已被定义为 kendo 模块的依赖项。

图 2 中,下一个模块(即我命名为 app 的模块)中将加载代码“require([], function(){})”。注意,我有意给模块起了有意义的名称。

那么,SPA 如何知道要先调用此模块呢?您需要在 SPA 的第一个登录页面中,用 RequireJS 的脚本引用标签中的 data-main 属性来进行配置。我指定了让 SPA 运行主模块 (main.js)。RequireJS 将在加载此模块时处理相关的所有繁重任务;您只需要告诉它先加载哪个模块即可。

对于要加载到 SPA 中的 SPA 视图可使用两个选项:标准 HTML (*.html) 或 ASP.NET MVC Razor (*.cshtml) 页。因为本文面向的是 .NET 开发人员,并且很多企业都有一些他们想继续使用的服务器端库和框架,所以我将选择第二种选项,即创建 Razor 视图。

我首先添加一个视图并将它命名为 Spa.cshtml(正如前面提到的)。此视图主要加载 SPA 布局的外壳或所有 HTML。在此视图中,当用户在 SPA 中导航时,我将通过交换替换“content”div 中所有 HTML 的视图来加载这些视图(例如 About.cshtml、Contact.cshtml、Index.cshtml 等等)。

创建 SPA 登录页面(布局)(Northwind.Web/Views/Spa.cshtml) 因为 Spa.cshtml 视图是您将加载所有其他视图的 SPA 登录页面,除了引用所需样式表和 RequireJS 之外不会有很多标记。注意以下代码中的 data-main 属性,该属性告诉 RequireJS 先加载哪个模块:

@{
  ViewBag.Title = "Spa";
  Layout = "~/Views/Shared/_Layout.cshtml";
}
<link href=
  "~/Content/kendo/2013.3.1119/kendo.common.min.css" 
  rel="stylesheet" />
<link href=
  "~/Content/kendo/2013.3.1119/kendo.bootstrap.min.css" 
  rel="stylesheet" />
<script src=
  "@Url.Content("~/scripts/require.js")"
  data-main="/scripts/app/main"></script>
<div id="app"></div>

为 SPA 布局添加操作 (Northwind.Web/­Controllers/HomeController.cs) 要创建和加载 Spa.cshtml 视图,请添加一个操作和视图:

public ActionResult Spa()
{
  return View();
}

创建应用程序模块 (Northwind.Web/Scripts/app/app.js) 以下是应用程序模块,负责初始化和启动 Kendo UI 路由器:

define([
    'router'
  ], function (router) {
    var initialize = function() {
      router.start();
    };
    return {
      initialize: initialize
    };
  });

创建路由器模块 (Northwind.Web/Scripts/app/router.js) 这个模块由 app.js 调用。如果您已经熟悉 ASP.NET MVC 路由,那么这里要讲的概念是相同的。这些是您的视图的 SPA 路由。我将定义所有 SPA 视图的所有路由,这样,当用户在 SPA 中导航时,Kendo UI 路由器就知道要将哪些视图加载到 SPA 中。请参阅随附下载中的列表 1

Kendo UI 路由器类负责跟踪应用程序状态和在不同应用程序状态之间导航。路由器使用 URL 的片段 (#page) 集成到浏览器历史记录中,这使应用程序变为可存为标签和可链接的状态。单击可路由的 URL 时,路由器将开始工作,告诉应用程序将自己重新置于被编码为进行路由的状态。路由定义是一个字符串,表示用于确定用户希望看到的应用程序状态的路径。当路由定义与浏览器的 URL 的 hash 段匹配时,将调用路由处理程序(请参见图 3)。

图 3 已注册的路由定义和相应的 URL

已注册的路由(定义) 实际的完整(可存为书签)URL
/ localhost:25061/home/spa/home/index
/home/index localhost:25061/home/spa/#/home/index/home/about
/home/about localhost:25061/home/spa/#/home/about/home/contact
/home/contact localhost:25061/home/spa/#/home/contact/customer/index
/customer/index localhost:25061/home/spa/#/customer/index

至于 Kendo UI 布局小部件,其名称本身就说明了其用途。当您创建新 ASP.NET MVC Web 应用程序时,您可能已经熟悉项目中包含的 ASP.NET Web 表单 MasterPage 或 MVC 布局。在此 SPA 项目中,它位于路径 Northwind.Web/Views/Shared/_Layout.cshtml 中。在 Kendo UI 布局与 MVC 布局之间没多大区别,不过 Kendo UI 布局在客户端运行。就像布局在服务器端的工作方式一样,MVC 运行时用其他视图换出布局的内容,Kendo UI 布局也是如此更新其内容。您使用 showIn 方法换出 Kendo UI 布局的视图(内容)。视图内容 (HTML) 将用 ID“Content”放入 div 中,此内容初始化时传递到 Kendo UI 布局中。初始化布局之后,您使用 ID“app”将它显示在 div 中,这是登录页面 (Northwind.Web/Views/Home/Spa.cshtml) 中的 div。稍后我会再进行讨论。

loadView 帮助程序方法加载一个视图模型(一个视图,或者还可以在发生视图和视图模型绑定时根据需要加载对调用的回调)。在 loadView 方法内,您可在视图交换过程中添加一些简单的动画,使用 Kendo UI FX 库来提高用户的外观审美体验。这可通过将当前加载的视图滑动到左侧、远程加载到新视图中,然后将新加载的视图滑回中央部分来实现。显然,您可以使用 Kendo UI FX 库轻松对此加以变换,创造各种各样不同的动画效果。当您调用 showIn 方法来换出视图时,使用 Kendo UI 布局的一个关键的好处就显示出来了。它将确保正确卸载、摧毁视图以及从浏览器的 DOM 中删除视图,从而确保 SPA 的扩展能力和高性能。

编辑 _ViewStart.cshtml 视图 (Northwind.Web/Views/­_ViewStart.cshtml) 下面演示了如何将所有视图配置为默认情况下不使用 ASP.NET MVC 布局:

@{
  Layout = null;
}

此时,SPA 应该能够工作。当单击菜单上的任何导航链接时,您会看到当前内容将通过 AJAX 换出,这要归功于 Kendo UI 路由器和 RequireJS。

将全新 ASP.NET Web 应用程序转换为 SPA 所需要的这七个步骤还不错吧?

现在 SPA 已启动并在运行,我将去做大部分开发人员都会使用 SPA 所做的事情,那就是添加创建、读取、更新和删除 (CRUD) 功能。

向 SPA 添加 CRUD 功能

下面是向 SPA 添加 Customer 网格视图(和相关项目代码文件)所需要的关键步骤:

  • 添加一个 CustomerController MVC 控制器 (Northwind.Web/Controllers/CustomerController.cs)。
  • 添加一个 REST OData Customer Web API 控制器 (Northwind.Web/Api/CustomerController.cs)。
  • 添加一个 Customer 网格视图 (Northwind.Web/Views/­Customer/Index.cshtml)。
  • 添加一个 CustomerModel 模块 (Northwind.Web/Scripts/app/models/CustomerModel)。
  • 为 Customer 网格添加一个 customerDatasource 模块 (Northwind.Web/Scripts/app/datasources/customer­Datasource.js)。
  • 为 Customer 网格视图添加一个 indexViewModel 模块 (Northwind.Web/Scripts/app/viewModels/­indexViewModel.js)。

使用 Entity Framework 设置解决方案结构图 4 显示了解决方案结构,其中突出了三个项目:Northwind.Data (1)、Northwind.Entity (2) 和 Northwind.Web (3)。我将简单介绍每个项目和 Entity Framework Power Tools。

  • Northwind.Data:此项目包含与 Entity Framework 对象-关系映射 (ORM) 工具相关的一切,以便实现持久保存。
  • Northwind.Entity:此项目包含由 Plain Old CLR 对象 (POCO) 类构成的域实体。这些实体全都是非持久域对象。
  • Northwind.Web:这包括 ASP.NET MVC 5 Web 应用程序、表示层,您将使用两个之前提到过的库(Kendo UI 和 RequireJS)
  • 以及服务器端体系的其余部分(Entity Framework、Web API 和 OData)在该层构建 SPA。
  • Entity Framework Power Tools:为创建所有 POCO 实体和映射(数据库优先),我使用了 Entity Framework 团队所提供的 Entity Framework Power Tools (bit.ly/1cdobhk)。代码生成之后,我所做的只是将这些实体复制到不同的项目 (Northwind.Entity) 以处理不同的问题而已。

A Best-Practice Solution Structure
图 4 最佳实践解决方案结构

注:Northwind SQL 安装脚本和数据库备份都包含在 Northwind.Web/App_Data 文件夹下的可下载源代码 (bit.ly/1cph5qc) 中。

现在已经设置好解决方案以便访问数据库,我将编写 MVC CustomerController.cs 类来提供索引和编辑视图。因为控制器的唯一职责是提供 SPA 的 HTML 视图,此处的代码会非常简短。

创建 MVC Customer 控制器 (Northwind.Web/­Controllers/CustomerController.cs) 下面说明了如何通过对索引和编辑视图的操作来创建 Customer 控制器:

public class CustomerController : Controller
{
  public ActionResult Index()
  {
    return View();
  }
  public ActionResult Edit()
  {
    return View();
  }
}

使用 Customers 网格 (Northwind.Web/­Views/Customers/Index.cshtml) 创建视图 图 5 展示了如何使用 Customers 网格创建视图。

如果您对图 5 中的标记不熟悉,不要惊慌,这些不过是 Kendo UI MVVM (HTML) 标记而已。 它仅用于配置 HTML 元素,在此例中是配置 ID 为“grid”的 div。稍后当您将此视图绑定到具有 Kendo UI MVVM 框架的视图模型时,会将此标记转换为 Kendo UI 小部件。 您可以在 bit.ly/1d2Bgfj 上详细了解相关信息。

图 5 带有 MVVM 小部件和事件绑定的 Customer 网格视图标记

<div class="demo-section">
  <div class="k-content" style="width: 100%">
    <div id="grid"
      data-role="grid"
      data-sortable="true"
      data-pageable="true"
      data-filterable="true"
      data-editable="inline"
      data-selectable="true"
      data-toolbar='[ { template: kendo.template($("#toolbar").html()) } ]'
      data-columns='[
        { field: "CustomerID", title: "ID", width: "75px" },
        { field: "CompanyName", title: "Company"},
        { field: "ContactName", title: "Contact" },
        { field: "ContactTitle", title: "Title" },
        { field: "Address" },
        { field: "City" },
        { field: "PostalCode" },
        { field: "Country" },
        { field: "Phone" },
        { field: "Fax" } ]'
      data-bind="source: dataSource, events:
        { change: onChange, dataBound: onDataBound }">
    </div>
    <style scoped>
    #grid .k-toolbar {
      padding: 15px;
    }
    .toolbar {
      float: right;
    }
    </style>
  </div>
</div>
<script type="text/x-kendo-template" id="toolbar">
  <div>
    <div class="toolbar">
      <span data-role="button" data-bind="click: edit">
        <span class="k-icon k-i-tick"></span>Edit</span>
      <span data-role="button" data-bind="click: destroy">
        <span class="k-icon k-i-tick"></span>Delete</span>
      <span data-role="button" data-bind="click: details">
        <span class="k-icon k-i-tick"></span>Edit Details</span>
    </div>
    <div class="toolbar" style="display:none">
      <span data-role="button" data-bind="click: save">
        <span class="k-icon k-i-tick"></span>Save</span>
      <span data-role="button" data-bind="click: cancel">
        <span class="k-icon k-i-tick"></span>Cancel</span>
    </div>
  </div>
</script>

创建 MVC (OData) Web API Customer (Northwind.Web/Api/CustomerController.cs) 现在我将演示如何创建 MVC (OData) Web API Customer 控制器。 OData 是一个 Web 数据访问协议,提供了一种统一的方式,以通过 CRUD 操作来查询和操纵数据。 使用 ASP.NET Web API 可以轻松创建 OData 端点。 您可以控制要显示的 OData 操作。 您可以将多个 OData 端点与非 OData 端点一起托管。 您对数据模型、后端业务逻辑和数据层有完全控制权。 图 6 显示了 Customer Web API OData 控制器的代码。

图 6 中的代码只创建了一个 OData Web API 控制器以显示来自 Northwind 数据库的 Customer 数据。 创建之后,您可以运行项目,然后使用如 Fiddler (fiddler2.com 上的一个免费 Web 调试程序)或 LINQPad 之类的工具来查询客户数据。

图 6 Customer Web API OData 控制器

public class CustomerController : EntitySetController<Customer, string>
{
  private readonly NorthwindContext _northwindContext;
  public CustomerController()
  {
    _northwindContext = new NorthwindContext();
  }
  public override IQueryable<Customer> Get()
  {
    return _northwindContext.Customers;
  }
  protected override Customer GetEntityByKey(string key)
  {
    return _northwindContext.Customers.Find(key);
  }
  protected override Customer UpdateEntity(string key, Customer update)
  {
    _northwindContext.Customers.AddOrUpdate(update);
    _northwindContext.SaveChanges();
    return update;
  }
  public override void Delete(string key)
  {
    var customer = _northwindContext.Customers.Find(key);
    _northwindContext.Customers.Remove(customer);
    _northwindContext.SaveChanges();
  }
}

配置和显示网格的 Customer 表中的 OData (Northwind.Web/App_Start/WebApiConfig.cs) 图 7 配置和显示网格的 Customer 表中的 OData。

使用 LINQPad 查询 OData Web API 如果您未使用过 LINQPad (linqpad.net),请将此工具添加到您的开发人员工具包中;它是必备工具,且可以免费获得。 图 8 显示了连接到 Web API OData (localhost:2501/odata) 的 LINQPad,其中显示了 LINQ 查询“Customer.Take (100)”的结果。

图 7 配置 OData 的 ASP.NET MVC Web API 路由

public static void Register(HttpConfiguration config)
{
  // Web API configuration and services
  ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
  var customerEntitySetConfiguration =
    modelBuilder.EntitySet<Customer>("Customer");
  customerEntitySetConfiguration.EntityType.Ignore(t => t.Orders);
  customerEntitySetConfiguration.EntityType.Ignore(t =>
     t.CustomerDemographics);
  var model = modelBuilder.GetEdmModel();
  config.Routes.MapODataRoute("ODataRoute", "odata", model);
  config.EnableQuerySupport();
  // Web API routes
  config.MapHttpAttributeRoutes();
  config.Routes.MapHttpRoute(
    "DefaultApi", "api/{controller}/{id}",
    new {id = RouteParameter.Optional});
}

Querying the Customer Controller Web API OData Via a LINQPad Query
图 8 通过 LINQPad 查询来查询 Customer 控制器 Web API OData

创建(可观察的)Customer 模型 (Northwind.Web/­Scripts/app/models/customerModel.js) 下面介绍如何创建(Kendo UI 可观察的)Customer 模型。您可以将它视为一个客户端 Customer 实体域模型。我创建了该 Customer 模型,以便 Customer 网格视图和编辑视图都可以轻松重用它。代码如图 9 所示。

图 9 创建 Customer(Kendo UI 可观察的)模型

define(['kendo'],
  function (kendo) {
    var customerModel = kendo.data.Model.define({
      id: "CustomerID",
      fields: {
        CustomerID: { type: "string", editable: false, nullable: false },
        CompanyName: { title: "Company", type: "string" },
        ContactName: { title: "Contact", type: "string" },
        ContactTitle: { title: "Title", type: "string" },
        Address: { type: "string" },
        City: { type: "string" },
        PostalCode: { type: "string" },
        Country: { type: "string" },
        Phone: { type: "string" },
        Fax: { type: "string" },
        State: { type: "string" }
      }
    });
    return customerModel;
  });

为 Customers 网格创建数据源 (Northwind.Web/Scripts/app/datasources/customersDatasource.js) 如果您熟悉来自 ASP.NET Web 表单的数据源,那么此处的概念也是一样的,您要为 Customer 网格创建数据源 (Northwind.Web/Scripts/app/datasources/customersDatasource.js)。 Kendo UI DataSource (bit.ly/1d0Ycvd) 组件是对使用本地(JavaScript 对象数组)或远程(XML、JSON 或 JSONP)数据的一种抽象。 它完全支持 CRUD 数据操作,并为排序、分页、筛选、分组和聚合提供本地和服务器端支持。

为 Customers 网格视图创建视图模型 如果您已经在 Windows Presentation Foundation (WPF) 或 Silverlight 中熟悉了 MVVM,则现在所说的是完全相同的概念,在客户端(在 Northwind.Web/Scripts/ViewModels/­Customer/indexViewModel.cs 中的此项目中可找到)。 MMVM 是一种体系结构分隔模式,用于将视图、视图数据与业务逻辑分隔开。 您一会儿将看到,所有数据、业务逻辑等都在视图模型中,且视图是纯 HTML 形式(可显示)。 图 10 显示了用于 Customer 网格视图的代码。

图 10 Customer 网格视图模型

define(['kendo', 'customerDatasource'],
  function (kendo, customerDatasource) {
    var lastSelectedDataItem = null;
    var onClick = function (event, delegate) {
      event.preventDefault();
      var grid = $("#grid").data("kendoGrid");
      var selectedRow = grid.select();
      var dataItem = grid.dataItem(selectedRow);
      if (selectedRow.length > 0)
        delegate(grid, selectedRow, dataItem);
      else
        alert("Please select a row.");
      };
      var indexViewModel = new kendo.data.ObservableObject({
        save: function (event) {
          onClick(event, function (grid) {
            grid.saveRow();
            $(".toolbar").toggle();
          });
        },
        cancel: function (event) {
          onClick(event, function (grid) {
            grid.cancelRow();
            $(".toolbar").toggle();
          });
        },
        details: function (event) {
          onClick(event, function (grid, row, dataItem) {
            router.
navigate('/customer/edit/' + dataItem.CustomerID);
          });
        },
        edit: function (event) {
          onClick(event, function (grid, row) {
            grid.editRow(row);
            $(".toolbar").toggle();
          });
        },
        destroy: function (event) {
          onClick(event, function (grid, row, dataItem) {
            grid.dataSource.remove(dataItem);
            grid.dataSource.sync();
          });
        },
        onChange: function (arg) {
          var grid = arg.sender;
          lastSelectedDataItem = grid.dataItem(grid.select());
        },
        dataSource: customerDatasource,
        onDataBound: function (arg) {
          // Check if a row was selected
          if (lastSelectedDataItem == null) return;
          // Get all the rows     
          var view = this.dataSource.view();
          // Iterate through rows
          for (var i = 0; i < view.length; i++) {
          // Find row with the lastSelectedProduct
            if (view[i].CustomerID == lastSelectedDataItem.CustomerID) {
              // Get the grid
              var grid = arg.sender;
              // Set the selected row
              grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']"));
              break;
            }
          }
        },
      });
      return indexViewModel;
  });

我将简要介绍图 10 中代码的各个不同组成部分:

  • onClick(帮助程序):此方法是一个帮助程序函数,它获取 Customer 网格的一个实例、当前所选行以及所选行的 Customer 的表示的 JSON 模型。
  • save:它在进行 Customer 的行内编辑时保存更改。
  • cancel:它取消行内编辑模式。
  • details:它将 SPA 导航到 Customer 编辑视图,将 Customer 的 ID 追加到 URL。
  • edit:它针对当前所选 Customer 激活行内编辑。
  • destroy:它删除当前所选 Customer。
  • onChange(事件):它会在每次选中一个 Customer 时触发。您将保存最后所选的 Customer,以便保持状态。在执行任何更新或导航离开 Customer 网格之后,当导航回该网格时,您可重新选择最后所选的 Customer。

现在将 customerModel、indexViewModel 和 customersDatasource 模块添加到您的 RequireJS 配置 (Northwind.Web/Scripts/app/main.js)代码如图 11 所示。

图 11 RequireJS 配置添加

paths: {
  // Packages
  'jquery': '/scripts/jquery-2.0.3.min',
  'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
  'text': '/scripts/text',
  'router': '/scripts/app/router',
  // Models
  'customerModel': '/scripts/app/models/customerModel',
  // View models
  'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
  'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
  // Data sources
  'customerDatasource': '/scripts/app/datasources/customerDatasource',
  // Utils
  'util': '/scripts/util'
}

为新 Customers 网格视图添加路由 注意,在 loadView 回调(在 Northwind.Web/Scripts/app/router.js 中)中,您将在网格工具栏初始化且 MVVM 绑定已经完成后绑定该工具栏。 这是因为您首次绑定网格时,工具栏尚未初始化,因为它存在于网格中。 当网格通过 MVVM 首次初始化时,它将从 Kendo UI 模板加载到工具栏中。 当它加载到网格中时,您只需将工具栏绑定到视图模型,这样工具栏中的按钮就会绑定到视图模型中的 save 和 cancel 方法。 下面是注册 Customer 编辑视图的路由定义的相关代码:

router.route("/customer/index", function () {
  require(['customer-indexViewModel', 'text!/customer/index'],
    function (viewModel, view) {
      loadView(viewModel, view, function () {
        kendo.bind($("#grid").find(".k-grid-toolbar"), viewModel);
      });
    });
});

您现在有了一个功能齐全的 Customers 网格视图。在浏览器中加载 localhost:25061/Home/Spa#/customer/index(在您的计算机上此端口号可能不一样),您会看到图 12

The Customer Grid View with MVVM Using the Index View Model
图 12 使用索引视图模型的 MVVM Customer 网格视图

连接 Customers 编辑视图 下面是将 Customer 编辑视图添加到 SPA 的关键步骤:

  • 创建一个通过 MVVM 绑定到 Customer 模型的 Customer 编辑视图 (Northwind.Web/Views/Customer/Edit.cshtml)。
  • 为 Customer 编辑视图添加一个编辑视图模型模块 (Northwind.Web/Scripts/app/viewModels/­editViewModel.js)。
  • 添加一个实用帮助程序模块,用于从 URL (Northwind.Web/Scripts/app/util.js) 获取 ID。

因为您在使用 Kendo UI 框架,请继续使用 Kendo UI 样式来设计编辑视图。您可在 bit.ly/1f3zWuC 上详细了解相关信息。图 13 显示了具有 MVVM 小部件和事件绑定的编辑视图标记。

图 13 具有 MVVM 小部件和事件绑定的编辑视图标记

<div class="demo-section">
  <div class="k-block" style="padding: 20px">
    <div class="k-block k-info-colored">
      <strong>Note: </strong>Please fill out all of the fields in this form.
</div>
    <div>
      <dl>
        <dt>
          <label for="companyName">Company Name:</label>
        </dt>
        <dd>
          <input id="companyName" type="text"
            data-bind="value: Customer.CompanyName" class="k-textbox" />
        </dd>
        <dt>
          <label for="contactName">Contact:</label>
        </dt>
        <dd>
          <input id="contactName" type="text"
            data-bind="value: Customer.ContactName" class="k-textbox" />
        </dd>
        <dt>
          <label for="title">Title:</label>
        </dt>
        <dd>
          <input id="title" type="text"
            data-bind="value: Customer.ContactTitle" class="k-textbox" />
        </dd>
        <dt>
          <label for="address">Address:</label>
        </dt>
        <dd>
          <input id="address" type="text"
            data-bind="value: Customer.Address" class="k-textbox" />
        </dd>
        <dt>
          <label for="city">City:</label>
        </dt>
        <dd>
          <input id="city" type="text"
            data-bind="value: Customer.City" class="k-textbox" />
        </dd>
        <dt>
          <label for="zip">Zip:</label>
        </dt>
        <dd>
          <input id="zip" type="text"
            data-bind="value: Customer.PostalCode" class="k-textbox" />
        </dd>
        <dt>
          <label for="country">Country:</label>
        </dt>
        <dd>
          <input id="country" type="text"
          data-bind="value: Customer.Country" class="k-textbox" />
        </dd>
        <dt>
          <label for="phone">Phone:</label>
        </dt>
        <dd>
          <input id="phone" type="text"
            data-bind="value: Customer.Phone" class="k-textbox" />
        </dd>
        <dt>
          <label for="fax">Fax:</label>
        </dt>
        <dd>
          <input id="fax" type="text"
            data-bind="value: Customer.Fax" class="k-textbox" />
        </dd>
      </dl>
      <button data-role="button"
        data-bind="click: saveCustomer"
        data-sprite-css-class="k-icon k-i-tick">Save</button>
      <button data-role="button" data-bind="click: cancel">Cancel</button>
      <style scoped>
        dd
        {
          margin: 0px 0px 20px 0px;
          width: 100%;
        }
        label
        {
          font-size: small;
          font-weight: normal;
        }
        .k-textbox
        {
          width: 100%;
        }
        .k-info-colored
        {
          padding: 10px;
          margin: 10px;
        }
      </style>
    </div>
  </div>
</div>

创建一个实用程序,以从 URL 获取 Customer 的 ID 因为您将创建具有清晰边界的简洁模块,以将问题进行合理的分隔,我将演示如何创建一个您的所有实用帮助程序都在其中的 Util 模块。 我将启动一个 utility 方法,它可以检索 Customer 数据源 (Northwind.Web/Scripts/app/datasources/customerDatasource.js) 的 URL 中的客户 ID,如图 14 所示。

图 14 实用工具模块

define([],
  function () {
    var util;
    util = {
      getId:
      function () {
        var array = window.location.href.split('/');
        var id = array[array.length - 1];
        return id;
      }
    };
    return util;
  });

将编辑视图模型和 Util 模块添加到 RequireJS 配置 (Northwind.Web/Scripts/app/main.js)图 15 中的代码显示了为 Customer 编辑模块添加的 RequireJS 配置。

图 15 为 Customer 编辑模块添加的 RequireJS 配置

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router',
    // Models
    'customerModel': '/scripts/app/models/customerModel',
    // View models
    'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
    'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
    // Data sources
    'customerDatasource': '/scripts/app/datasources/customerDatasource',
    // Utils
    'util': '/scripts/util'
    },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
  });
require([
  'app'
], function (app) {
  app.initialize();
});

添加 Customer 编辑视图模型 (Northwind.Web/Scripts/app/viewModels/editViewModel.js)图 16 中的代码显示了如何添加 Customer 编辑视图模型。

图 16 Customer 视图的 Customer 编辑视图模型模块

define(['customerDatasource', 'customerModel', 'util'],
  function (customerDatasource, customerModel, util) {
    var editViewModel = new kendo.data.ObservableObject({
      loadData: function () {
        var viewModel = new kendo.data.ObservableObject({
          saveCustomer: function (s) {
            customerDatasource.sync();
            customerDatasource.filter({});
            router.
navigate('/customer/index');
          },
          cancel: function (s) {
            customerDatasource.filter({});
            router.
navigate('/customer/index');
          }
        });
        customerDatasource.filter({
          field: "CustomerID",
          operator: "equals",
          value: util.getId()
        });
        customerDatasource.fetch(function () {
          console.log('editViewModel fetching');
          if (customerDatasource.view().length > 0) {
            viewModel.set("Customer", customerDatasource.at(0));
          } else
            viewModel.set("Customer", new customerModel());
        });
        return viewModel;
      },
    });
    return editViewModel;
  });

我将简要介绍图 16 中代码的各个不同组成部分:

  • saveCustomer:此方法负责保存对 Customer 的所有更改。它还重置 DataSource 的筛选器,以便在网格中并入所有 Customer。
  • cancel:此方法将 SPA 导航回 Customer 网格视图。它还重置 DataSource 的筛选器,以便在网格中并入所有 Customer。
  • filter:它调用 DataSource 的筛选器方法,并用 URL 中的 ID 查询特定 Customer。
  • fetch:它在设置筛选器后调用 DataSource 的 fetch 方法。在 fetch 的回调中,需使用 Datasource fetch 所返回的 Customer 来设置视图模型的 Customer 属性,将使用该属性来绑定到 Customer 编辑视图。

当 RequireJS 加载模块时,“define”方法主体内的代码只会调用一次(在 RequireJS 加载模块时调用),因此您将在编辑视图模型中显示一个方法 (loadData),以便您在加载编辑视图模型模块之后能够加载数据(请参见 Northwind.Web/­Scripts/app/router.js)。

为新的 Customer 编辑视图 (Northwind.Web/Scripts/app/router.js) 添加路由 下面是添加路由器的相关代码:

router.route("/customer/edit/:id",
        function () {
    require(['customer-editViewModel',
          'text!/customer/edit'],
      function (viewModel, view) {
      loadView(viewModel.loadData(), view);
    });
  });

请注意,从 RequireJS 请求 Customer 编辑视图模型时,您可以通过从视图模型调用 loadData 方法获取 Customer。这样您就能够在每次加载 Customer 编辑视图时基于每一个 URL 中的 ID 加载正确的 Customer 数据。路由不一定是一个硬编码的字符串。它也可以包含参数,如后端服务器路由器(Ruby on Rails、ASP.NET MVC、Django 等等)。为此,您可以在您想用的变量名称之前使用一个冒号,如此来命名路由段。

现在可以在浏览器中加载 Customer 编辑视图 (localhost:25061/Home/Spa#/customer/edit/ANATR),您将看到图 17 所示的屏幕。

The Customer Edit View
图 17 Customer 编辑视图

注:虽然对 Customer 网格视图的 delete (destroy) 功能已经可用,但当单击工具栏中的“Delete”按钮(请参见图 18)时,您将看到异常,如图 19 所示。

The Customer Grid View
图 18 Customer 网格视图

Expected Exception When Deleting a Customer Due to CustomerID Foreign Key Referential Integrity
图 19 删除 Customer 时由于 CustomerID 外键引用完整性会遇到的异常**

此异常是本身固有的,因为大多数 Customer ID 都是其他表(例如 Orders、Invoices 等)中的外键。您必须进行级联删除,从以 Customer ID 为外键的所有表中删除所有记录。虽然您无法任意删除,我还是想演示一下删除功能的步骤和代码。

我已提供了这些步骤和代码。我演示了使用 RequireJS 和 Kendo UI 将现成的 ASP.NET Web 应用程序转换为 SPA 是如何快捷。然后我演示了将 CRUD 这样的功能添加到 SPA 是如何轻松。

您可以在 bit.ly/1bkMAlK 看到实时项目演示,在 easyspa.codeplex.com 看到 CodePlex 项目站点和可下载的代码。

祝您工作愉快!

Long Le 是 CBRE Inc. 的 .NET 应用/开发团队的首席架构师,也是一位 Telerik/Kendo UI MVP。他主要致力于开发框架和应用程序块,提供最佳实践、模式方面的指导,以及标准化企业的技术体系。他致力于改进 Microsoft 的技术方面的工作已经超过 10 年了。他在业余时间喜欢撰写博客 (blog.longle.net) 和玩 Call of Duty 游戏。您可以在 twitter.com/LeLong37 访问和关注他的 Twitter。

衷心感谢以下技术专家对本文的审阅:Derick Bailey (Telerik) 和 Mike Wasson (Microsoft)