使用 Knockout.js 和服务器端定义的视图模型构建 ASP.NET 应用程序






4.91/5 (9投票s)
让我们构建一个简单的框架,它可以在不编写任何 JavaScript 代码的情况下获得 knockout.js 的强大功能。
介绍
Knockout.js 是一个开源的 JavaScript 库,它允许将模型-视图-视图模型 (MVVM) 架构模式应用于网页。它是一个强大的库,可以简化具有许多用户交互的复杂页面的开发。
唯一的问题是客户端需要编写更多的代码,而每个人都知道,在客户端编写代码比在服务器端编写代码更困难、更耗时(智能提示支持较少,没有编译时错误检查等)
这个小型应用程序将展示如何在服务器端代码 (C#) 中定义模型及其主要功能,而不会损失客户端编程的强大功能。这样,代码将更加结构化,并且在部署之前会对其一致性进行更多的编译时检查。
在此示例中,我们演示了如何在不编写任何 JavaScript 代码的情况下构建复杂的响应式界面。当然,将一些处理移到客户端(使用计算字段和函数)会提高性能,但在模型不大时,差异可能非常小。
背景
在阅读本文之前,熟悉 Knockout.js 库非常重要。有关它的更多信息,请参阅官方网站。
使用代码
示例应用程序是一个 Visual Studio 2010 Web 应用程序,它以分页方式显示订单及其详细信息。您可以对订单执行一些基本操作(更改数据、编辑主要数据、更改价格、数量、发货状态)并保存它。订单数据保存在 XML 文件 Order.xml 中,该文件存储在根目录下。
页面布局在 KnockoutServerSideViewModel.ascx 用户控件中编码。此用户控件从 XML 文件加载模型,并将其存储在用户控件基类 (BaseKOUserControl
) 自动创建的隐藏字段中。另一个视图模型定义在 DateTime.ascx 用户控件中,只是为了演示在同一页面中使用由单独视图模型运行的更多用户控件。
来自 KnockoutServerSideViewModel.ascx:
<%@ Control Language="C#" AutoEventWireup="true"
CodeBehind="KnockoutServerSideViewModel.ascx.cs"
Inherits="KnockoutServerSideViewModel.Web.KnockoutServerSideViewModel" %>
<%@ Register src="Pager.ascx" tagname="Pager" tagprefix="uc1" %>
<table id="bindingArea" border="1" cellpadding="10">
<tr>
<td>
First name:
</td>
<td>
<span data-bind="text: FirstName, visible: !MainEditing()" ></span>
<input data-bind="value: FirstName, visible: MainEditing()" ></input>
</td>
</tr>
<tr>
<td>
Last name:
</td>
<td>
<span data-bind="text: LastName, visible: !MainEditing() " ></span>
<input data-bind="value: LastName, visible: MainEditing()" ></input>
</td>
</tr>
<tr>
<td>
Date:
</td>
<td>
<span data-bind="text: LastSavedTime" ></span>
</td>
</tr>
<tr data-bind="visible:TimesSaved()>0">
<td>
Saved:
</td>
<td>
<span data-bind="text:TimesSaved"></span> time
<span data-bind="visible:TimesSaved()>1">s</span>
</td>
</tr>
<tr>
<td colspan="2">
<input type="button" value="Edit main data"
data-bind="visible: !MainEditing(), click: c$.bind($data, 'MainEdit')" ></input>
<input type="button" value="Close main data"
data-bind="visible: MainEditing(), click: c$.bind($data, 'MainClose') " ></input>
</td>
</tr>
<tr>
<td colspan="3">
<input type="button" value="Save"
data-bind="click: c$.bind($data, 'Save')" />
<input type="button" value="Save and close"
data-bind="click: c$.bind($data, 'SaveAndClose')" />
</td>
</tr>
<tr>
<td colspan="2">
<table border="1" cellpadding="5">
<tr>
<td colspan="10">
<uc1:Pager ID="Pager2" runat="server" />
</td>
</tr>
<tr>
<td>
Code
</td>
<td>
Name
</td>
<td>
Quantity
</td>
<td>
Price
</td>
<td>
Total
</td>
<td>
Delivered
</td>
</tr>
<tbody data-bind="foreach: Details">
<tr>
<td>
<span data-bind="text: Key" />
</td>
<td>
<span data-bind="text: Name, visible: !Editing()"></span>
<input type="text" data-bind="value: Name, visible: Editing()" />
</td>
<td>
<span data-bind="text: Quantity, visible: !Editing()"></span>
<select data-bind="value: Quantity, visible: Editing()">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</td>
<td>
<span data-bind="text: UnitPrice, visible: !Editing()"></span>
<input type="text" data-bind="value: UnitPrice, visible: Editing()"></input>
<span data-bind="visible: PriceNotValid()" style="color: Red">Price not valid</span>
</td>
<td>
<span data-bind="text: Total" />
</td>
<td>
<span data-bind="text: Delivered" />
</td>
<td>
<input type="button" value="Set delivered"
data-bind="click: $root.c$.bind($data, 'SetDelivered', $data.Key()), visible: Delivered() == false" />
<input type="button" value="Set not delivered"
data-bind="click: $root.c$.bind($data, 'SetNotDelivered', $data.Key()), visible: Delivered() == true" />
<input type="button" value="Increase price (2 €)"
data-bind="click: $root.c$.bind($data, 'AddTwoEuro', $data.Key())" />
<br />
<input type="button" value="Edit"
data-bind="click: $root.c$.bind($data, 'Edit', $data.Key()), visible: !Editing()" />
<input type="button" value="Close"
data-bind="click: $root.c$.bind($data, 'Close', $data.Key()), visible: Editing()" />
<input type="button" value="Delete"
data-bind="click: $root.c$.bind($data, 'Delete', $data.Key())" />
</td>
</tr>
</tbody>
<tr>
<td colspan="10">
<uc1:Pager ID="Pager1" runat="server" />
</td>
</tr>
</table>
</td>
</tr>
</table>
来自 KnockoutServerSideViewModel.ascx.cs:
public partial class KnockoutServerSideViewModel : BaseKOUserControl
{
protected override void OnInit(EventArgs e)
{
ViewModel = OrderViewModel.GetPagedOrder(1,10);
base.OnInit(e);
}
}
来自 BaseKOUserControl.cs:
private BaseViewModel _ViewModel;
public BaseViewModel ViewModel
{
get
{
return _ViewModel;
}
set
{
_ViewModel = value;
_ViewModel.ModelClass = string.Format("{0}.{1}, {2}",
_ViewModel.GetType().Namespace, _ViewModel.GetType().Name,
_ViewModel.GetType().Assembly.GetName().FullName);
}
}
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
writer.WriteLine(string.Format("<span id=\"sp{0}\">", this.ClientID));
base.Render(writer);
writer.WriteLine(string.Format("</span>"));
writer.WriteLine(string.Format("<script type=\"text/javascript\">" +
"\n$().ready(function () {{ var model = setModelFunction(\"{0}\", " +
"\"{1}\"); model.init(); ko.applyBindings(model.viewModel, " +
"document.getElementById(\"sp{0}\")); }});\n</script>",
this.ClientID, this.ControlName));
writer.WriteLine(string.Format("<input type=\"hidden\" id " +
"= \"hd{0}\" value=\"{1}\" />",
this.ClientID, HttpUtility.HtmlEncode(Utilities.ConvertToJson(ViewModel))));
}
重写了 Render
方法,以便添加必要的 JavaScript 代码来启动 knockout 绑定。一个包含模型(序列化为 JSON)的隐藏字段会在用户控件的末尾添加,并被一个 span 包围,该 span 允许进行部分 knockout 绑定,因此您可以在同一页面中拥有更多具有服务器端绑定的用户控件,它们不会互相干扰。
ViewModel
对象继承了 BaseViewModel
,这是一个简单的类,定义如下:
public class BaseViewModel
{
public BaseModel() { }
public string Function { get; set; }
public string ModelClass { get; set; }
public string Argument { get; set; }
public string RedirectUrl { get; set; }
}
ModelClass
属性包含 ViewModel 所用类的完整 .NET 描述(包括程序集和命名空间),它在页面加载期间,在 ViewModel 定义的开头设置:
_ViewModel.ModelClass = string.Format("{0}.{1}, {2}",
_ViewModel.GetType().Namespace, _ViewModel.GetType().Name, _ViewModel.GetType().Assembly.GetName().FullName);
这很重要,因为 ViewModel 实例是通过 Web 服务从页面传递到服务器端处理的,了解类描述允许将其反序列化为正确的类型。
RedirectUrl
属性用于在调用服务器端函数后告知页面重定向到新 URL(例如,请参阅 SaveAndClose()
方法)。
Web 服务位于 default.aspx 页面:
[WebMethod]
public static string CallModelFunction(string jsmodel, string className)
{
Type t = System.Type.GetType(className);
BaseModel model = Utilities.ConvertFromJson(jsmodel, t) as BaseModel;
t.InvokeMember(model.Function, System.Reflection.BindingFlags.InvokeMethod, System.Type.DefaultBinder, model, null);
return Utilities.ConvertToJson(model);
}
BaseViewModel
类定义了一个名为 Function
的属性,该属性包含要在服务器端调用的函数。假设该函数作为模型类(在本例中模型类为 OrderViewModel
)的非静态公共成员存在。例如,模型中的 Save()
方法定义为 OrderViewModel
类的非静态公共成员:
public void Save()
{
this.TimesSaved++;
this.LastSavedTime = DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString();
PersistModel();
}
模型中的所有函数都可以定义为 void,因为将更新后的模型返回页面的责任委托给了服务。
为了在页面中正确定义 viewModel(我们现在在客户端),我们使用了 ko.mapping 插件,该插件接受一个 JSON 字符串并将其反序列化为具有可观察字段的模型。
这是 KnockOutBaseManager.js 文件,其中包含映射模型(包含在 viewModel 隐藏字段中)并将绑定应用于页面的所有必要功能。它还包含 CallModelFunction
函数,该函数管理与服务器端应用程序的通信(它将序列化模型传递给服务并将返回的模型映射到页面)。c$ 函数可以放置在 knockout 绑定中以调用 viewModel 中的任何服务器端函数。它还接受一个附加参数,该参数放置在 ViewModel 类的 Argument
属性中(例如,用于标识我们单击了哪个行的按钮)。
function callFunction(functionName, viewModel) {
viewModel.Function(functionName);
$.ajax({
type: "POST",
url: "/Default.aspx/CallModelFunction",
data: ko.toJSON({ jsmodel: ko.toJSON(viewModel), className: viewModel.ModelClass() }),
contentType: "application/json",
success: function (result) {
ko.mapping.fromJSON(result, viewModel);
if (viewModel.RedirectUrl() != "" && viewModel.RedirectUrl() != null) {
document.location.href = viewModel.RedirectUrl();
}
}
});
}
function setModelFunction(area, name) {
var model = function (area) {
var serviceUrl = "/Default.aspx/";
//var proxy = new ServiceProxy(serviceUrl);
var viewModel = ko.mapping.fromJSON($("#hd" + area).val());
viewModel.c$ = function (functionName) {
var argument = arguments[1];
if (typeof argument == 'string' || typeof argument == 'number') {
viewModel.Argument(argument);
}
callFunction(functionName, viewModel);
}
var init = function () {
if (typeof window["setup_" + name] == 'function') {
window["setup_" + name](viewModel);
//setup(viewModel);
}
};
return {
init: init,
viewModel: viewModel
};
} (area);
return model;
}
BaseViewModel
类包含一个名为 Argument
的属性,用于传递服务器端函数执行其工作所需的任何自定义值。例如,在 AddTwoEuro
函数中,它用于传递价格增加应用的详细信息的索引(当然,您也可以使用它来传递增加值)。
模型定义位于 Order.cs 文件中。服务器端代码实际上没有经过优化,也不是很优雅,但示例的目的是构建一个优雅的数据访问层,所以我决定不在这里浪费太多时间(如果您对此感到冒犯,请接受我诚挚的道歉 )
因此,总结一下,如果您想定义一个带有自己的模型的新用户控件,您需要遵循以下步骤:
- 创建一个继承自
BaseKOUserControl
的用户控件。它的名称将是类的名称,或者您可以在Page_Init
方法中通过设置Control
名称属性的值来更改它。 - 定义一个继承自
BaseViewModel
的viewModel
类。 - 在
Page_Load
方法中定义 ViewModel 属性。 - 在用户控件的 ASCX 文件中创建带有绑定的 HTML 代码。
- 向 ViewModel 类添加必要的函数。
- 运行它!
此工具是在 70 年代一支前卫摇滚乐队“Banco del Mutuo Soccorso”的宝贵帮助下创建的
再见!