数据点

在 JavaScript 中轻松地访问数据 -- 没错,就是 JavaScript

Julie Lerman

下载代码示例

Julie Lerman尽管在进行软件开发时我觉得自己就像个“管道工”,但我也需要执行相当多的客户端开发工作。甚至在本专栏中,我冒险地使用了涉及数据访问的客户端应用程序。但当客户端是 JavaScript 时,这从来不是轻而易举的事 — 很遗憾,我的 JavaScript 技能仍嫌不足,并且每个学习曲线对我来说(以及作为我的诉苦对象的 Twitter 关注者来说)都很痛苦。但在我千辛万苦取得成功之际,就会发现所付出的努力始终是值得的。而且,对于能够帮助我在 JavaScript 中更轻松地工作的任何内容,我都衷心欢迎。因此,在单页应用程序上进行 Vermont.NET 用户组演示的过程中,当来自 IdeaBlade 的 Ward Bell 为我们演示了他和他的团队为 JavaScript 精心打造的公开源数据访问 API 后,我对此很感兴趣。从我在实体框架方面的经验来说,我所看到的展示相当于将实体框架用于客户端 Web 开发。该 API 称作 Breeze,在撰写本文时仍处于测试状态。Bell 慷慨地抽出宝贵时间来帮助我了解 Breeze 的更多信息,以便为本专栏提供丰富的内容。您可以从 breezejs.com 中获取这些信息,在这个网址中,您还将找到令人印象深刻的各种文档、视频和示例。

在我于 2012 年 6 月的数据点专栏“使用 Knockout.js 绑定 Web 应用程序中的 OData 数据”(msdn.microsoft.com/magazine/jj133816) 中,着重介绍了如何使用 Knockout.js 更轻松地在客户端执行数据绑定。Breeze 可与 Knockout 无缝结合使用,因此,我将再次讨论 6 月专栏中的示例。我的目标是了解 Breeze 的引入如何在该示例的工作流中简化我的编码工作:

  • 从服务器获取数据。
  • 绑定和展示这些数据。
  • 将更改推送回服务器。

我将逐一介绍更新解决方案的关键部分,以便您了解这些不同的部分如何彼此配合而达成目的。如果您想要跟踪某个恰当确立的解决方案并且对各方面进行彻底检验,则可以从archive.msdn.microsoft.com/mag201212DataPoints 下载完整解决方案。

原始示例

下面是我之前采取的解决方案的主要步骤:

  • 在客户端,我定义了一个 person 类,Knockout 可以使用该类来进行数据绑定:
function PersonViewModel(model) {
  model = model || {};
  var self = this;
  self.FirstName = ko.observable(model.Name || ' ');
  self.LastName = ko.observable(model.Name || ' ');
}
  • 我的数据是通过 OData 数据服务提供的,因此我使用了 datajs 来访问这些数据,datajs 是用于从 JavaScript 使用 OData 的一种工具包。
  • 我获取了查询结果(以 JSON 形式返回)并且使用这些值创建了一个 PersonViewModel 实例。
  • 然后,我的应用程序让 Knockout 处理数据绑定,同时协调用户所做的更改。
  • 我采用了修改后的 PersonViewModel 实例并且根据该实例的值更新了我的 JSON 对象。
  • 最后,我将该 JSON 对象传递到 datajs,以便通过 OData 保存到服务器上。

我甚至没有为相关数据而费心,因为相关数据会导致这个小示例复杂得多。

使用 ASP.NET Web API 的更新服务

使用 Breeze,我可以对我的 OData 服务执行 HTTP 调用,或者对 ASP.NET Web API 定义的服务执行 HTTP 调用 (asp. net/web-api)。 我将我的服务切换到了 ASP.NET Web API,它针对与我以前使用的实体框架模型相同的模型,只是添加了一样东西。 我以前的示例仅公开 Person 数据。 我现在具有 Device 类形式的相关数据,因为我知道的每名开发人员都拥有少量个人设备。 我的 ASP.NET Web API 公开的相关函数是返回 Person 数据的 GET、针对 Device 数据的另一个 GET 以及用于保存更改的单个 POST。 我还使用 Metadata 函数公开我的数据架构,如图 1 中所示。 Breeze 将使用此 Metadata 以便理解我的模型。

图 1 我的 Web API 服务的主要组成部分

readonly EFContextProvider<PersonModelContext> _contextProvider =
  new EFContextProvider<PersonModelContext>();
[AcceptVerbs("GET")]
public IQueryable<Person> People()
{
  return _contextProvider.Context.People;
}
[AcceptVerbs("GET")]
public IQueryable<Device> Devices()
{
  return _contextProvider.Context.Devices;
}
[AcceptVerbs("POST")]
public SaveResult SaveChanges(JObject saveBundle)
{
  return _contextProvider.SaveChanges(saveBundle);
}
[AcceptVerbs("GET")]
public string Metadata()
{
  return _contextProvider.Metadata();
}

服务器上的 Breeze.NET

请注意在这些方法中使用的 _contextProvider 变量。 我没有直接调用我的 EF DbContext (PersonModelContext) 的方法。 而是使用 Breeze EFContextProvider 对它进行了包装。 这正是 _contextProvider.Metadata 方法来自的位置,以及将接受 saveBundle 参数的 SaveChanges 的签名。 使用 saveBundle,Breeze 将允许我从我的应用程序发送一组数据修改,它将把这些修改传递到我的 DbContext 上以便永久保存到数据库。

我已将我的 ASP.NET Web API 应用程序命名为“BreezyDevices”,这样,我现在可以使用 http://localhost:19428/api/breezydevices/metadata 请求架构。 并且,我可以通过指定 GET 方法之一来查询数据: http://localhost:19428/api/breezydevices/people。

因为客户端上的 Breeze 将查询并保存到 ASP.NET Web API 远程服务,所以,我可以从我的客户端应用程序上删除 datajs。

Breeze 将如何帮助我的示例

就这个示例而言,我将使用 Breeze 来针对三个令人头痛的问题:

  1. 我的服务返回并且接受单纯的 JSON,但我需要将 JavaScript 对象与 Knockout 可观察量属性一起使用,以便数据绑定到 UI。
  2. 我想要包括相关数据,但这在客户端上很难。
  3. 我需要将多个更改发送到服务器以便进行保存。

使用 Breeze,我可以直接数据绑定到我的结果数据。 我将 Breeze 配置为使用 Knockout,为此,它将在后台为我创建 Knockout 可观察量属性。 这意味着使用相关数据会简单得多,因为我不必将其从 JSON 转换为可绑定对象,并且不必执行额外的工作来使用我的查询结果在客户端上重新定义图形。

有一些服务器端配置涉及使用 Breeze。 我会将这方面的详细信息保留在 Breeze 文档上,这样,我就可以将重心放在示例的客户端数据访问部分上。 此外,因为对于 Breeze 而言,它所提供的功能要比我将在这个示例中所利用的功能多得多,所以,在您从本文中对它所提供的好处浅尝辄止后,将会想要访问 breezejs.com 来了解详细信息。

图 2 显示 Breeze 适用于服务器端以及客户端上的工作流的地方。

The Breeze.NET API Helps on the Server While the BreezeJS API Helps on the Client
图 2 Breeze.NET API 在服务器上提供帮助,而 BreezeJS API 在客户端提供帮助

从 Breeze 查询

我在 OData 以及实体框架上的经验使我对使用 Breeze 进行查询很熟悉。 我将使用 Breeze EntityManager 类。 EntityManager 能够读取您的服务的元数据提供的数据模型,并且自己生成 JavaScript“entity”对象;您不必定义实体类或撰写映射程序。

还要进行一些客户端配置。 例如,下面的代码段创建针对某些 Breeze 命名空间的快捷方式,然后将 Breeze 配置为使用 Knockout 和 ASP.NET Web API:

var core = breeze.core,
           entityModel = breeze.entityModel;
core.config.setProperties({
  trackingImplementation: entityModel.entityTracking_ko,
  remoteAccessImplementation: entityModel.remoteAccess_webApi
});

可以将 Breeze 配置为使用多种其他形式的绑定框架(例如 Backbone.js 或 Windows JavaScript 库)和数据访问技术(例如 OData)。

接下来,我将创建知道我的服务的相对 URI 的 EntityManager。 EntityManager 相当于实体框架或 OData 上下文。 它充当我的针对 Breeze 的网关并且对数据进行缓存:

var  manager = new entityModel.EntityManager('api/breezydevices');

现在,我可以定义一个查询并且让 EntityManager 来为我执行该查询。 在使用 Entity Framework 和 LINQ to Entities 上,或者在使用任何 OData 客户端 API 上,此查询代码都差别不大;因此,在学习如何使用 Breeze 中,这些代码颇受我的喜爱:

function getAllPersons(peopleArray) {
    var query = new entityModel.EntityQuery()
      .from("People")
      .orderBy("FirstName, LastName");
    return manager
      .executeQuery(query)
      .then(function (data) {
        processResults(data,peopleArray); })
      .fail(queryFailed);
  };

我在客户端上执行这些查询代码并且可以通过异步方式执行我的查询,这就是为什么 executeQuery 方法允许我定义在查询成功执行时要执行的操作 (.then) 以及失败时要执行的操作 (.fail)。

请注意,我在将一个数组(稍后您将看到的 Knockout 可观察量数组)传递到 getAllPersons。 如果查询成功执行,我会将该数组传递到 processResults 方法,该方法将清空该数组,然后使用来自服务的数据填充该数组。 以前,我不得不遍历结果并且自己创建每个 PersonViewModel 实例。 通过 Breeze,我可以直接使用这些返回的数据:

function processResults(data, peopleArray) {
    var persons = data.results;
    peopleArray.removeAll();
    persons.forEach(function (person) {
      peopleArray.push(person);
    });
  }

这将向我提供我将在视图中展示的 person 对象的数组。

getAllPersons 函数位于我称作 dataservice 的一个对象内。 我将在代码的下一个部分中使用 dataservice。

自填充视图模型

在来自我的六月 Knockout 文章的示例中,查询和结果有别于我在该视图中用于数据绑定的 PersonViewModel 类。 因此,我执行了该查询,并且使用我编写的映射代码将结果转换到 PersonViewModel 实例中。 因为我不需要使用 Breeze 映射代码或 PersonViewModel,所以,我这次将使我的应用程序稍微智能一点,并且使其显示我的 dataservice 从数据库提取的 Person 对象的数组。 为了反映这一点,我现在将一个对象命名为 PeopleViewModel。 这公开了我已定义为 Knockout 可观察量数组的 people 属性,我使用 dataservice.getAllPersons 填充该属性:

(function (root) {
  var app = root.app;
  var dataservice = app.dataservice;
  var vm = {
    people: ko.observableArray([]),
    }
  };
  dataservice.getAllPersons(vm.people);
  app.peopleViewModel = vm;
}(window));

在下载示例中,您将找到一个称作 main.js 的文件,该文件是针对应用程序逻辑的起始点。 它包含称作 Knockout applyBindings 方法的以下代码行:

ko.applyBindings(app.peopleViewModel, $("content").get(0));

applyBindings 方法通过在视图中声明的数据绑定将视图模型属性和方法连接到 HTML UI 控件。

此示例中的视图是我的 index.cshtml 中的一小块 HTML。 请注意在 people 数组中绑定和显示每个 person 对象的名字和姓氏的 Knockout 数据绑定标记:

    <ul data-bind="foreach: people">
      <li class="person" >
        <label data-bind="text: FirstName"></label>
        <label data-bind="text: LastName"></label>
      </li>
    </ul>

在运行我的应用程序时,我获取了我的个人数据的只读视图,如图 3 中所示。

Using Breeze and Knockout to Easily Consume Data in JavaScript
图 3 使用 Breeze 和 Knockout 轻松地在 JavaScript 中使用数据

调整 JavaScript 和 Knockout 以便允许编辑

您可能还记得六月的专栏,Knockout 可便于绑定数据以便进行编辑。 与 Breeze 一起使用,这是很棒的组合,以便轻松地编辑数据以及将数据永久保存回服务器。

首先,我将一个函数添加到调用 Breeze manager.saveChanges 方法的 dataservice 对象。 调用后,该 Breeze EntityManager 汇总挂起的更改并且将它们发布到 Web API 服务:

function saveChanges() {
     manager.saveChanges();
  }

然后,我会将新的 saveChanges 函数作为 dataservice 的功能公开:

var dataservice = {
    getAllPersons: getAllPersons,
    saveChanges: saveChanges,
  };

现在,我的 PeopleViewModel 对象需要公开自己的保存方法以便绑定到该视图;该视图模型保存函数委托给 dataservice saveChanges 方法。 在这里,我使用 JavaScript“匿名函数”定义视图模型保存:

var vm = {
    people: ko.observableArray([]),
    save: function () {
      dataservice.saveChanges();
    },
  };

接下来,我用输入元素(文本框)替换我的标签,以便用户可以编辑 Person 对象。 我必须从“text”切换到 Knockout“value”关键字,以便实现与用户输入的双向绑定。 我还添加了一个图像,该图像具有绑定到 PeopleViewModel.save 方法的单击事件:

<img src="../../Images/save.png" 
  data-bind="click: save" title="Save Changes" />
<ul data-bind="foreach: people">
  <li class="person" >
    <form>
      <label>First: </label><input 
        data-bind="value: FirstName" />
      <label>Last: </label> <input 
        data-bind="value: LastName" />
    </form>
  </li>
</ul>

就是这样。 Breeze 和 Knockout 将执行其余操作! 您可以在图 4 中看到为进行编辑而显示的数据。

Using Breeze to Save Data via JavaScript
图 4 使用 Breeze 通过 JavaScript 保存数据

我可以编辑其中任何字段或所有字段并且单击保存按钮。 该 Breeze EntityManager 将会汇总所有数据更改并且将这些更改向上推送到服务器,再将它们发送到实体框架以便更新数据库。 尽管我将不会对这个演示进行扩展以便包括插入和删除,但 Breeze 当然也可以处理这些修改。

对于大结局 — 添加相关数据

这在任何 JavaScript 应用程序中都是令许多开发人员头痛不已的一部分 — 而这正是我要撰写本专栏的原因。

我将对我的脚本进行一个小更改,并且在窗体中添加一个小标记,该标记将每个人都转换为可编辑的 master/­detail person。

我的脚本中的更改将位于 dataservice 中,其中,我将通过添加 Breeze 扩展查询方法来修改该查询,以便将每个人员的设备连同该人员一起预先加载。 扩展是您从 OData 或 NHibernate 中可能会认识的一个术语,它类似于实体框架中的 Include(Breeze 还支持在事后轻松地加载相关数据):

var query = new entityModel.EntityQuery()
           .from("People")
           .expand("Devices")
           .orderBy("FirstName, LastName");

然后,我将修改我的视图以便它知道如何显示设备数据,如图 5 中所示。

图 5 修改视图以便显示设备数据

<ul data-bind="foreach: people">
  <li class="person" >
    <form>
      <label>First: </label><input 
        data-bind="value: FirstName" />
      <label>Last: </label> <input 
        data-bind="value: LastName" />
      <br/>
      <label>Devices: </label>
      <ul class="device" data-bind="foreach: Devices">
        <li>
          <input data-bind="value: DeviceName"/>
        </li>
      </ul>                     
    </form>
  </li>
</ul>

至此您已实现了您的目的。 如您在图 6 中所见,Breeze 将处理客户端上图形的预先加载和生成。 它还可以对数据进行协调以便发送回服务来进行更新。 在服务器端,Breeze EFContextProvider 将对它接收的所有更改数据进行整理,并且确保实体框架获取将数据永久保存到数据库所需的内容。

Consuming and Saving Related Data
图 6 使用和保存相关数据

尽管这对于使用一对多关系来说比较简单,但在撰写本文之际,该 Breeze 的测试版不支持多对多关系。

轻松的客户端数据访问

Bell 告诉我,激发他开发 Breeze 的原因就是他在处理既大量使用 JavaScript 又大量进行数据访问的项目上的痛苦体验。 他的公司 IdeaBlade 始终致力于创建解决方案以便解决处理断开连接的数据问题,并且开发人员已能够将丰富的经验应用于这个开放源项目。 我一直不愿意接纳使用许多 JavaScript 的项目,因为我的技能限制了我着手使用,并且我知道数据访问部分会让我不高兴。 我一看到 Breeze 就对它很感兴趣。 尽管我只是介绍了 Breeze 的一些皮毛,但是我在本文中向您展现的最后这一部分的内容(展现了它在使用和保存相关数据上是多么容易)真的让我刮目相看。

Julie Lerman是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。 您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。 她是《Programming Entity Framework》(2010) 以及“代码优先”版 (2011) 和 DbContext 版 (2012)(均出自 O’Reilly Media)的作者,博客网址为 thedatafarm.com/blog。 请关注她的 Twitter:twitter.com/julielerman

衷心感谢以下技术专家对本文的审阅: Ward Bell