65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ASP.NET MVC、jQuery 和 Knockout.js 进行客户端模型绑定

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (10投票s)

2010年12月23日

CPOL

10分钟阅读

viewsIcon

117406

downloadIcon

2175

演示如何使用 Knockout.js 和 jQuery 模板来管理 JavaScript 模型绑定

引言

Knockout 是一个新推出的 JavaScript 库,它简化了客户端的数据绑定。它可以以类似于 Silverlight 绑定工作的方式,将用户界面和数据模型保持同步。它在原理上类似于 jQuery 中的数据链接,但将该概念推向了更远。它是由 Steven Sanderson 编写的,他也是 ASP.NET MVC 方面最好的(在我看来)书籍的作者之一,因此它具有良好的背景。它旨在补充 jQuery 等其他库的功能,当它们一起使用时,可以轻松地实现一些非常强大的行为。

必备组件

要运行代码,您需要安装了 MVC 的 Visual Studio 2008 或更高版本。相关的 JavaScript 文件已包含在示例项目中,但供参考,我使用的是 Knockout v1.1.1 和 jQuery 模板,可以从 此处 下载,以及 jQuery 1.4.4。

之前

本文承接了我之前写的一篇关于在 ASP.NET MVC 中使用列表框的文章。供参考,用户界面如下所示:

UI

总结一下行为,用户选择可用列表框中的一个或多个项目,然后点击“>>”按钮将它们移动到所需的列表框。完成此操作后,所选项目的详细信息将显示在列表框下方。

目前,用户界面完全由 Web 服务器驱动,当按下按钮时,表单会提交,服务器将采取必要的操作来更新视图数据模型,然后重新显示更新后的用户界面。这工作得足够好,但对于这种行为来说,由客户端驱动会是更好的用户体验;每次都不需要真正涉及服务器来重绘界面。在本文中,我们将增强当前的用户界面,使其所有显示工作都在客户端完成,除了初始设置和最终提交之外,不再涉及服务器。我们将通过渐进增强的方式来实现,以便页面也能在不支持 JavaScript 的浏览器中正常工作。

定义行为

为了了解我们的目标,让我们定义一些页面应表现出的行为,其中一些与当前服务器驱动的行为重复。

  1. 选择项目并按下相应的传输按钮应将项目移动到另一个列表框。
  2. 如果列表框中没有选择项目,则相应的传输按钮应被禁用。
  3. 当项目从一个框移动到另一个框时,产品详细信息列表应更新以反映新的选择。
  4. 如果当前选择不符合业务规则,则发送按钮应被禁用。

乍一看,这些要求可能并不算太棘手,但请考虑必须保持同步的所有元素;手动完成的代码可能会很快变得混乱。而且页面中没有足够的信息来生成产品详细信息列表,因此我们需要诉诸 AJAX 或手动管理 JavaScript 对象来满足第 3 点。Knockout 可以显著简化这类任务。

Using the Code

让我们从行为 #1 开始。我们需要做的第一件事是为页面定义一个视图模型,该对象模拟用户界面的状态和行为,Knockout 使用它来执行绑定。在实践中,它有点像 Web 窗体页面中的代码隐藏,但更具动态性。为了表示列表框的状态,我们需要每个列表框的 2 条信息:列表框中的项目和用户已选择的项目。我们希望 Knockout 能够跟踪这些值,因此我们将它们创建为可观察数组。可观察数组是一个包装标准数组的 Knockout 对象,当其中对象的数量发生变化时,它会发出通知。然后,我们的视图模型看起来像这样:

var viewModel = {
            availableProducts: ko.observableArray
		(<%=new JavaScriptSerializer().Serialize(Model.AvailableProducts) %> ),
            availableSelected: ko.observableArray([]),
            requestedProducts: ko.observableArray
		(<%=new JavaScriptSerializer().Serialize(Model.RequestedProducts) %>),
            requestedSelected: ko.observableArray([])
}

在此代码中,我使用了 JavaScriptSerializer 类(位于 System.Web.Script.Serialization 命名空间中)将 JSON 数据注入数组。或者,我们可以通过 AJAX 请求到一个返回 JsonResult 的操作方法来获取数据。

Knockout 通过将 data-bind 属性应用于 HTML 元素来描述所需行为,虽然这不应引起任何浏览器问题,但确实会引发一些潜在问题:

  • 在非 HTML 5 的场景中,“data-bind”不被视为有效属性,并会导致 W3C 验证器出现验证错误。
  • 因为它不是一个有效的 C# 属性名,所以我们不能使用匿名对象初始化器语法来为使用 HTML 帮助程序创建的 HTML 添加属性,取而代之的是,我们必须使用更丑陋的字典初始化器语法。
  • 有些人就是不喜欢将这类信息与 HTML 标记混合在一起。

其中一些问题可以通过使用 jQuery 的 attr 命令来应用属性来缓解。为了简单起见,在本文的其余部分,我将直接应用属性,但示例下载包含一个“纯 HTML”版本,该版本通过了 XHTML 1.1 验证(尽管方式有些脏),并且所有工作都在外部 JavaScript 文件中完成。

为了连接列表框的数据,我们将使用 3 个绑定:

  • options:告诉 Knockout 我们视图模型中的哪个属性代表列表框的选项。
  • selectedOptions:告诉 Knockout 我们视图模型中的哪个属性代表列表框的选定选项。
  • optionsText:告诉 Knockout 数组中对象的哪个属性用于显示选项文本。

要将这些属性应用于 select 列表,请修改对 Html.ListBoxFor() 的调用。

<%=Html.ListBoxFor(m => m.AvailableSelected, 
                        new MultiSelectList(Model.AvailableProducts, 
			"Id", "Name", Model.AvailableSelected),
                  		new Dictionary<string, object>{{"data-bind",
			"options:availableProducts, selectedOptions:availableSelected, 
			optionsText:'Name'"}})%>		

请注意,未提供 options value;理解一个重要的概念是,绑定跟踪底层数据模型,而不仅仅是 UI 元素中包含的值。当我们选择列表框中的一个选项时,视图模型中的 selectedOptions 数组将填充对应产品项的所有数据,而不仅仅是我们传统上从 HTML select 元素的值/文本格式中能获得的 id 和 name。这是一个非常强大的功能,正如我们将看到的。

要启动 Knockout,请在页面就绪处理程序中添加命令 ko.applyBindings(viewModel);。重新加载页面,然后……嗯,在视觉上应该没有变化,但如果你在 FireBug 或类似工具中检查列表框,你就能看到列表框数据中的变化,表明 Knockout 正在工作。

好了,让我们添加一些行为。我们将向我们的移动按钮应用一个新的绑定,即**click binding**。将两个 submits 修改为以下内容:

<input type="submit" name="add" value=">>" data-bind="click: addRequested" />
<input type="submit" name="remove" value="<<" data-bind="click: removeRequested" />

click 按钮指定了一个**在视图模型中**的函数,在点击时运行。在视图模型中的 requestedSelected 属性之后添加以下函数:

addRequested: function(){
                var requested = this.requestedProducts;
                $.each(this.availableSelected(), function(n, item) {
                    requested.push(item);
                });
                this.availableProducts.removeAll(this.availableSelected());
                this.availableSelected.splice(0,this.availableSelected().length);
            },

jQuery 的 each() 函数用于处理 selected 数组中的每个项目,并将其移动到 requested 数组中,然后从 available 数组中删除这些项目,并清空 selected 数组。Knockout 标准情况下会阻止默认操作发生,因此按下这些按钮时表单不会提交。removeRequested 的代码相同,只是使用的数组不同。

关于使用 observableArray 的说明

请注意,在上面的代码中,我有时使用不带括号的 requestedSelected ,有时使用带括号的 requestedSelected() observableArray 是数组的包装器,而不是数组本身,但可以通过使用 () 运算符访问底层数组。这可能会导致混淆,因此了解您正在访问哪个对象非常重要。observableArray 对象定义了一些函数,它们与普通数组对象的等效函数同名,例如 push() splice()。调用 requested.push() requested().push() 都会向底层数组添加一个项目;但是,Knockout 仅使用前一种语法来注册更改。您需要小心确保对可观察对象的所有更改都通过 observable 对象进行,而不是通过它包装的对象。并非所有函数都重复,因此,例如,length 必须通过 requestedSelected().length 访问。这种不一致性最初可能令人困惑,但总的来说,任何写入操作都应不带括号进行,任何读取操作都应带括号进行。

行为 #1 现在已完成:项目可以在列表框之间移动。为了实现这一点,我们不需要直接访问 UI 组件,而只需操作底层数据模型的值。这很巧妙,但远非革命性的;我们可以通过应用一些 DOM 操作更容易地实现相同的功能。然而,我们的视图模型现在几乎完成了,从这里开始实现其余的行为相对容易,我们很快就会看到这种方法的优势。

接下来是行为 #2:只有当列表框中有选定的项目时,传输按钮才应启用。将以下内容添加到“add”按钮:enable:availableSelected().length > 0,并将以下内容添加到 remove 按钮:enable:requestedSelected().length > 0。搞定,下一个!说真的,这有多容易?enable 绑定描述了元素应启用的条件。在这种情况下,相关的可观察数组应包含项目。Knockout 为我们处理了其余的事情。接下来是听起来棘手的行为 #3,使产品详细信息列表与用户请求的项目保持同步。为此,我们需要另一个绑定。

模板绑定

如果您查看 C# 代码中产品列表的渲染方式,它基本上包括为模型中 requested products 集合中的每个产品渲染一个表格行模板。Knockout 中的模板绑定允许我们使用客户端数据执行相同的操作。它在后台使用 jQuery Template 引擎(尽管您也可以选择其他引擎),因此如果您以前使用过它,这种格式应该很熟悉。首先,我们定义一个命名模板:

<script id="requestedTemplate" type="text/html">
<tr>
      	<td>${Name}</td>
        <td>${Description}</td>
        <td>${Price}</td>
      </tr>
</script>

那个 script 标签看起来有点奇怪(注意 type),但我认为它在做什么很明显。接下来,将模板绑定添加到 tbody 标签:

<tbody data-bind='template: { name: "requestedTemplate", foreach: requestedProducts }'>

这只是说,对视图模型中 requestedProducts 数组中的每个项目应用名为“requestedTemplate”的模板。这就是让产品列表正常工作的全部必要工作,它将自动与用户选择的任何项目保持同步。我们还需要更新所选产品的总数;为此,我们将创建一个依赖可观察对象,它是一个可观察值,其值本身依赖于其他可观察对象来确定其值。

viewModel.requestedTotal = ko.dependentObservable(function(){                
                var total = 0;
                $.each(this.requestedProducts(), function(n, item){
                    total = total + item.Price;
                });
                return total;
            }, viewModel);

然后将 value 绑定添加到 table 页脚的 span 中:

<span id="total" data-bind="text:requestedTotal().toFixed(2)">

现在,当用户移动项目时,总数将自动更新。

最后是行为 #4,只有当用户的选择符合我们的规则时,才启用发送按钮。我们已经足够了解了,所以这里不包含代码。创建依赖可观察对象来跟踪状态或将条件直接粘贴到 enable 属性中即可完成工作。

最后我们需要做的是在表单提交时将正确的信息发送到服务器。使用逗号分隔的 id 的隐藏字段用于跟踪用户请求了哪些产品。使用 submit 绑定,我们可以在表单提交之前更新此字段:

<form action='<%=Url.Action("Index") %>' method="post" data-bind="submit:onSubmit">

视图模型代码如下所示:

onSubmit: function(){                
                var ids = [];
                $.each(this.requestedProducts(), function(n, item) {
                    ids.push(item.Id);
                });
                $("#SavedRequested").val(ids.join());
                return true;
            }

从函数返回 true 将允许表单正常提交。

结论

Knockout 是走向更声明式编程风格的趋势的另一个例子:它允许我们说明我们想要发生什么,而无需指定如何实现。将其与 jQuery 结合使用,我们可以用很少的工作在我们的页面中实现一些非常好的客户端行为。ASP.NET MVC 让我们能够自由地控制我们的标记以及数据的接收方式,这使我们拥有了一个服务器端平台,能够更轻松地利用这些发展,并产生一些非常好的用户体验。

历史

  • 2010 年 12 月 22 日 - 首次上传
© . All rights reserved.