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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (9投票s)

2011 年 2 月 7 日

CPOL

9分钟阅读

viewsIcon

90689

downloadIcon

2102

如何在 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>

这里有几点我应该提到:

  1. 首先,您应该包含 _knockout-1.1.2.js_ 文件。
  2. 然后,您应该通过将 data-bind 属性添加到任何您喜欢的 HTML 元素来创建绑定。我相信这种语法很清楚。data-bind="visible: isRowVisible" 表示当 isRowVisible 属性为 true 时,该元素可见。isRowVisible 属性是做什么用的?稍等片刻。
  3. 最后,这是 KnockoutJS 的核心。在脚本块中,您应该创建一个 viewmodelviewmodel 是一个简单的 JavaScript 对象,其中包含属性。这些属性将用于绑定。isRowVisibleviewmodel 的一个属性。正如您所见,这些属性的值是使用 ko.observable 函数分配的。此函数允许系统跟踪属性的变化,并将这些变化发送到绑定的 HTML 元素。
  4. 最后一项是调用 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 的持久化。这是我们页面的完整代码:

KnockoutJsSample.aspx
<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>
KnockoutJsSample.aspx.cs
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">

这里有几件事情要做:

  1. 如何为每个页面/控件在主 View Model 中创建唯一的子模型名称?
  2. 如何将所有子模型合并到主 View Model 中?
  3. 如何实现子模型的持久化?

让我们解决这些问题。首先,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 文件中,我将把它附加到我们所有的页面/控件上:

KnockoutSupport.js
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> 

这是页面和控件的完整源代码:

KnockoutJsSample.aspx
<%@ 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>
TestControl.ascx
<%@ 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 对象的 RegisterClientScriptBlockRegisterStartupScript 方法。使用 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 的页面的示例:

KnockoutJsSample.aspx
<%@ 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>
KnockoutJsSample.aspx.cs
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:初始修订。
© . All rights reserved.