在 ASP.NET 应用程序中使用 KnockoutJS






4.94/5 (9投票s)
如何在 ASP.NET 应用程序中使用 KnockoutJS 库。
引言
最近,我偶然发现了一个很棒的 JavaScript 框架 KnockoutJS。它通过实现数据绑定功能来简化用户界面的开发。在本文中,我想简要介绍它,并讨论在 ASP.NET 应用程序中使用它时遇到的一些问题。
必备组件
要使用本文中的示例,您应该下载 KnockoutJS 的最新版本。此外,我使用 Visual Studio 2010 编写了我的代码。但我确信,使用 Visual Studio 2008 也将能够正常工作。
KnockoutJS 简介
让我们从一个非常简单的 KnockoutJS 使用示例开始。这是网页的代码:
<script type="text/javascript" src="Scripts/knockout-1.1.2.js"></script>
<table border="1">
<tbody><tr>
<td>
Change visibility of next row:
<input type="checkbox" data-bind="checked: isRowVisible" />
</td>
</tr>
<tr data-bind="visible: isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</tbody></table>
<script type="text/javascript">
var viewModel = { isRowVisible: ko.observable(false) };
ko.applyBindings(viewModel);
</script>
这里有几点我应该提到:
- 首先,您应该包含 _knockout-1.1.2.js_ 文件。
- 然后,您应该通过将
data-bind
属性添加到任何您喜欢的 HTML 元素来创建绑定。我相信这种语法很清楚。data-bind="visible: isRowVisible"
表示当isRowVisible
属性为 true 时,该元素可见。isRowVisible
属性是做什么用的?稍等片刻。 - 最后,这是 KnockoutJS 的核心。在脚本块中,您应该创建一个
viewmodel
。viewmodel
是一个简单的 JavaScript 对象,其中包含属性。这些属性将用于绑定。isRowVisible
是viewmodel
的一个属性。正如您所见,这些属性的值是使用ko.observable
函数分配的。此函数允许系统跟踪属性的变化,并将这些变化发送到绑定的 HTML 元素。 - 最后一项是调用
ko.applyBindings(viewModel)
。它会实现所有的魔法。
这些是 KnockoutJS 的基本用法。您可以参考网站上的官方文档了解更多信息。现在我想介绍在 ASP.NET 中使用 KnockoutJS。
绑定到 ASP.NET 控件
在前面的示例中,我使用了普通的 HTML input
元素。现在是时候用 ASP.NET 控件进行测试了。让我们尝试将其与 asp:Checkbox
一起使用。代码似乎
<asp:CheckBox ID="chkChangeVisibility" runat="server" data-bind="checked: isRowVisible" />
不起作用。当我们查看生成的 HTML 代码时,原因就很明显了:
<span data-bind="checked: isRowVisible">
<input id="chkChangeVisibility" type="checkbox" name="chkChangeVisibility" />
</span>
我们的 data-bind
属性不在 input
元素中。为了将其放置在 input
元素中,我们必须使用代码隐藏文件。例如:
protected void Page_Load(object sender, EventArgs e)
{
chkChangeVisibility.InputAttributes["data-bind"] = "checked: isRowVisible";
}
现在一切正常。
在回发之间保持 View Model
ASP.NET 页面执行回发到服务器是很常见的。如果我们从示例中执行回发,那么在回发后,您会发现所有更改都丢失了。这是 JavaScript 所做更改的常见问题。我们的 View Model 将从头开始重新加载,并重新设置初始状态。
但是,当然,我们希望保存所有更改。我建议的方法与 ASP.NET 中保持 ViewState 的方式非常相似。我们将 View Model 存储在一个隐藏字段中。让我们在页面中添加一个隐藏字段:
<input type="hidden" runat="server" id="hViewStateStorage" />
现在,我们应该将 View Model 写入此字段。在大多数情况下,控件的初始状态是在服务器端定义的。我们也将采用这种技术。在服务器端,我们将创建一个 View Model 对象,将其序列化为 JSON 格式,然后将 JSON 字符串放入隐藏字段中:
if (!IsPostBack)
{
var viewModel = new { isRowVisible = true };
var serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
var json = serializer.Serialize(viewModel);
hViewStateStorage.Value = json;
}
正如您所见,我在这里使用了一个匿名类。这意味着您甚至不需要在服务器端为 View Model 创建单独的类。
这里另一个诱人的可能性是更改 View Model 在回发期间的状态。因为它存储在隐藏字段中,您可以从中提取 View Model,反序列化它,分析并更改其属性,然后序列化它,并再次将其放入隐藏字段。在这种情况下,您需要为 View Model 创建一个单独的类才能将其反序列化。
现在,我们必须在 JavaScript 代码中从隐藏字段中提取 View Model。这是实现方法:
var stringViewModel = document.getElementById('<%=hViewStateStorage.ClientID %>').value;
var viewModel = ko.utils.parseJson(stringViewModel);
我在这里使用了 KnockoutJS 库中的 parseJson
函数将字符串表示形式转换为 JavaScript 对象。
但是现在我们遇到一个小问题。正如我所说,View Model 的所有属性都应该使用 ko.observable
函数进行初始化。现在情况不是这样。以下代码解决了这个问题:
for (var propertyName in viewModel) {
viewModel[propertyName] = ko.observable(viewModel[propertyName]);
}
现在 View Model 可以完美地从服务器接收。唯一需要做的是在回发之前将其保存到隐藏字段中。我使用了 jQuery 来订阅回发事件:
$(document.forms[0]).submit(function () {
document.getElementById('<%=hViewStateStorage.ClientID %>').value =
ko.utils.stringifyJson(viewModel);
});
您可能会认为这段代码会起作用。但它不会。第一次回发后,一切都停止工作了。似乎我们的隐藏字段现在包含一个没有任何属性的对象:
<input name="hViewStateStorage" type="hidden"
id="hViewStateStorage" value="{}" />
原因是什么?它在于 ko.observable
函数。实际上,它返回的是一个函数,而不是一个普通的值。这意味着我们 View Model 的所有属性现在都是函数。所以它们不会被序列化成 JSON 格式。为了将所有属性恢复到它们的“非函数”状态,我们必须使用 ko.utils.unwrapObservable
函数:
for (var propertyName in viewModel) {
viewModel[propertyName] = ko.utils.unwrapObservable(viewModel[propertyName]);
}
我们的目标实现了。我们在回发之间实现了 View Model 的持久化。这是我们页面的完整代码:
<script src="Scripts/knockout-1.1.2.js" type="text/javascript"></script>
<script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<input type="hidden" runat="server" id="hViewStateStorage" />
<table border="1">
<tr>
<td>
Change visibility of next row:
<asp:CheckBox ID="chkChangeVisibility" runat="server" />
</td>
</tr>
<tr data-bind="visible: isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</table>
<input runat="server" type="submit" />
<script type="text/javascript">
var stringViewModel =
document.getElementById('<%=hViewStateStorage.ClientID %>').value;
var viewModel = ko.utils.parseJson(stringViewModel);
for (var propertyName in viewModel) {
viewModel[propertyName] = ko.observable(viewModel[propertyName]);
}
ko.applyBindings(viewModel);
$(document.forms[0]).submit(function () {
for (var propertyName in viewModel) {
viewModel[propertyName] = ko.utils.unwrapObservable(viewModel[propertyName]);
}
document.getElementById('<%=hViewStateStorage.ClientID %>').value =
ko.utils.stringifyJson(viewModel);
});
</script>
public partial class KnockoutJsSample : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
chkChangeVisibility.InputAttributes["data-bind"] = "checked: isRowVisible";
if (!IsPostBack)
{
var viewModel = new { isRowVisible = true };
var serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
var json = serializer.Serialize(viewModel);
hViewStateStorage.Value = json;
}
}
}
在独立的 ASP.NET 控件和页面中使用 KnockoutJS
到目前为止一切都很顺利。但让我们考虑以下场景。我想在一个 ASP.NET 控件中使用 KnockoutJS。这个控件可以插入到一个也使用 KnockoutJS 的页面中。此外,我想在页面中放置此控件的多个实例。另外,我也可能在此页面上放置其他使用 KnockoutJS 的控件。我认为您已经看到了问题。具有可能相同属性名称的不同 View Model 如何在一个页面上工作?
嗯,KnockoutJS 有一个机制可以在同一页面上处理多个 View Model。您还记得 ko.applyBindings
函数吗?它可以接受第二个参数,即上下文:
ko.applyBindings(viewModel, document.getElementById('someDivId'));
此上下文是一个 DOM 元素。在这种情况下,元素将仅在该 DOM 元素内的 View Model 进行绑定。因此,您可以在页面上创建多个 div
并为每个 div
分配不同的 View Model。问题是这些 div
不能嵌套。在我看来,这是一个很大的限制,阻碍了 KnockoutJS 在 ASP.NET 中的自由使用。我们不能确定一个 View Model 不会干扰另一个。当然,我们可以非常仔细地设计我们的页面和控件,尽量避免嵌套使用 KnockoutJS 的控件,但无论如何,我认为这是一个潜在的错误来源。
我建议的方法是将页面上的所有 View Model 合并成一个 View Model。类似这样:
var mainViewModel = {
viewModelForPage: { someProperty: "someValue" },
viewModelForControl1: { anotherProperty: 1 },
viewModelForControl2: { anotherProperty: 2 },
};
在这种情况下,我们可以这样引用必要的属性:
<tr data-bind="visible: viewModelForControl2.anotherProperty">
这里有几件事情要做:
- 如何为每个页面/控件在主 View Model 中创建唯一的子模型名称?
- 如何将所有子模型合并到主 View Model 中?
- 如何实现子模型的持久化?
让我们解决这些问题。首先,ASP.NET 中已经有了唯一的名称。我指的是每个页面/控件的 ClientID
属性。我建议我们所有的子模型都将命名为相应页面/控件的 ClientID
。在这种情况下,我们可以这样引用子模型的属性:
<tr data-bind="visible: <%= this.ClientID %>.isRowVisible">
不幸的是,这种方法不适用于服务器端控件(带有 runat="server"
属性的控件)。对于这些控件,您应该在代码隐藏文件中设置 data-bind
属性:
chkChangeVisibility.InputAttributes["data-bind"] =
string.Format("checked: {0}.isRowVisible", this.ClientID);
考虑到有时我们必须使用 InputAttributes
而不是 Attributes
,无论如何这种方法都是不可避免的。
现在让我们看看将所有 Model 合并到一个 Model 中的系统。如果我们想在代码中只拥有一个实例,我们必须使用 Singleton 模式。但为了简单起见,我将只创建一个全局变量在一个 JavaScript 文件中,我将把它附加到我们所有的页面/控件上:
var g_KnockoutRegulator = {
ViewModel: {},
LoadViewModel: function (clientId, storageFieldId) {
var stringViewModel = document.getElementById(storageFieldId).value;
var clientViewModel = ko.utils.parseJson(stringViewModel);
var partOfBigViewModel = {};
this.ViewModel[clientId] = partOfBigViewModel;
for (var propertyName in clientViewModel) {
this.ViewModel[clientId][propertyName] =
ko.observable(clientViewModel[propertyName]);
}
$(document.forms[0]).submit(function () {
var newViewModel = {};
for (var propertyName in partOfBigViewModel) {
newViewModel[propertyName] =
ko.utils.unwrapObservable(partOfBigViewModel[propertyName]);
}
document.getElementById(storageFieldId).value =
ko.utils.stringifyJson(newViewModel);
});
ko.applyBindings(this.ViewModel);
}
};
让我们仔细看看这段代码。g_KnockoutRegulator
是一个 JavaScript 对象,带有一个 ViewModel
属性。此属性是我们绑定到所有控件的主 View Model。唯一的函数 LoadViewModel
完成了所有魔法。它获取当前页面/控件的 ClientID(clientId
)和隐藏存储字段的 ClientID(storageFieldId
)。在内部,它从存储字段中提取本地 View Model,并将其加载到主 View Model 中作为其一个属性(第一个 for
循环)。然后它订阅 Submit 事件,在该事件中执行相反的操作,将本地 View Model 存储到存储字段中。最后,它将主 View Model 绑定到控件。
使用 g_KnockoutRegulator
对象非常简单:
<script type="text/javascript">
g_KnockoutRegulator.LoadViewModel('<%= this.ClientID %>',
'<%=hViewStateStorage.ClientID %>');
</script>
这是页面和控件的完整源代码:
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="KnockoutJsSample.aspx.cs"
Inherits="KnockoutJsTest.KnockoutJsSample" %>
<%@ Register Src="~/UserControls/TestControl.ascx"
TagPrefix="uc" TagName="TestControl" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<script src="Scripts/knockout-1.1.2.js" type="text/javascript"></script>
<script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="Scripts/KnockoutSupport.js" type="text/javascript"></script>
<input type="hidden" runat="server" id="hViewStateStorage" />
<uc:TestControl runat="server" />
<table border="1">
<tr>
<td>
Change visibility of next row:
<asp:CheckBox ID="chkChangeVisibility" runat="server" />
</td>
</tr>
<tr data-bind="visible: <%= this.ClientID %>.isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</table>
<input runat="server" type="submit" />
<script type="text/javascript">
g_KnockoutRegulator.LoadViewModel('<%= this.ClientID %>',
'<%=hViewStateStorage.ClientID %>');
</script>
</form>
</body>
</html>
<%@ Control Language="C#" AutoEventWireup="true"
CodeBehind="TestControl.ascx.cs"
Inherits="KnockoutJsTest.UserControls.TestControl" %>
<script src="Scripts/knockout-1.1.2.js" type="text/javascript"></script>
<script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="Scripts/KnockoutSupport.js" type="text/javascript"></script>
<input type="hidden" runat="server" id="hViewStateStorage" />
<table border="1">
<tr>
<td>
Change visibility of next row:
<asp:CheckBox ID="chkChangeVisibility" runat="server" />
</td>
</tr>
<tr data-bind="visible: <%= this.ClientID %>.isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</table>
<script type="text/javascript">
g_KnockoutRegulator.LoadViewModel('<%= this.ClientID %>',
'<%=hViewStateStorage.ClientID %>');
</script>
正如您所见,页面和控件都使用了相同的 View Model 属性名称。但它们之间没有相互干扰。这正是我们想要的。
一些优化
我们创建了在 ASP.NET 中使用 KnockoutJS 的主框架。唯一我不喜欢的是调用 ko.applyBindings
。它为每个使用 KnockoutJS 的控件都调用了一次。但显然,调用一次就足够了。唯一需要考虑的是,此调用必须在所有控件都将本地 View Model 加载到主 View Model 中之后进行。我们如何实现这一点?我将这样修改 _KnockoutSupport.js_ 文件:
var g_KnockoutRegulator = {
NumberOfLocalViewModels: 0,
ViewModel: {},
LoadViewModel: function (clientId, storageFieldId) {
...
this.NumberOfLocalViewModels--;
if (this.NumberOfLocalViewModels == 0) {
ko.applyBindings(this.ViewModel);
}
}
};
在这里,字段 NumberOfLocalViewModels
必须设置为加载本地 View Model 的页面/控件的正确数量。我们如何获得这个数字?我将使用 ClientScriptManager
对象的 RegisterClientScriptBlock
和 RegisterStartupScript
方法。使用 RegisterClientScriptBlock
注册的所有代码都将在使用 RegisterStartupScript
注册的所有代码之前执行。所以这是我注册必要 JavaScript 代码的辅助类:
public class KnockoutJsHelper
{
public static void RegisterKnockoutScripts(Control control,
HtmlInputHidden storageField)
{
if (!control.Page.ClientScript.IsClientScriptIncludeRegistered("KnockoutJS"))
{
control.Page.ClientScript.RegisterClientScriptInclude("KnockoutJS",
control.Page.ResolveClientUrl(@"~\Scripts\knockout-1.1.2.js"));
}
if (!control.Page.ClientScript.IsClientScriptIncludeRegistered("jQuery"))
{
control.Page.ClientScript.RegisterClientScriptInclude("jQuery",
control.Page.ResolveClientUrl(@"~\Scripts\jquery-1.4.1.js"));
}
if (!control.Page.ClientScript.IsClientScriptIncludeRegistered("KnockoutRegulator"))
{
control.Page.ClientScript.RegisterClientScriptInclude("KnockoutRegulator",
control.Page.ResolveClientUrl(@"~\Scripts\KnockoutSupport.js"));
}
control.Page.ClientScript.RegisterClientScriptBlock(control.GetType(),
"IncreaseNumberOfViewModels" + control.ClientID,
"g_KnockoutRegulator.NumberOfLocalViewModels++;", true);
control.Page.ClientScript.RegisterStartupScript(control.GetType(),
"RegisterViewModelScripts" + control.ClientID,
string.Format("g_KnockoutRegulator.LoadViewModel('{0}', '{1}');",
control.ClientID, storageField.ClientID), true);
}
}
使用此类,我们不需要引用 _ .js_ 文件或手动调用 g_KnockoutRegulator
对象。最后两行完成了主要工作。第一行注册脚本,增加了要加载的本地 View Model 的数量。最后一行执行实际加载。这是一个使用此类 KnockoutJsHelper
的带有 KnockoutJS 的页面的示例:
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="KnockoutJsSample.aspx.cs"
Inherits="KnockoutJsTest.KnockoutJsSample" %>
<%@ Register Src="~/UserControls/TestControl.ascx"
TagPrefix="uc" TagName="TestControl" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<input type="hidden" runat="server" id="hViewStateStorage" />
<uc:TestControl runat="server" />
<table border="1">
<tr>
<td>
Change visibility of next row:
<asp:CheckBox ID="chkChangeVisibility" runat="server" />
</td>
</tr>
<tr data-bind="visible: <%= this.ClientID %>.isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</table>
<input runat="server" type="submit" />
</form>
</body>
</html>
public partial class KnockoutJsSample : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
chkChangeVisibility.InputAttributes["data-bind"] =
string.Format("checked: {0}.isRowVisible", this.ClientID);
if (!IsPostBack)
{
var viewModel = new { isRowVisible = true };
var serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
var json = serializer.Serialize(viewModel);
hViewStateStorage.Value = json;
}
}
protected override void OnPreRender(EventArgs e)
{
KnockoutJsHelper.RegisterKnockoutScripts(this, hViewStateStorage);
}
}
结论
这就是我想说的关于在 ASP.NET 中使用 KnockoutJS 的全部内容。希望它能为您提供一个良好的起点。谢谢!
历史
- 2011.07.02:初始修订。