使用 Spike & Knockout.js 远程数据绑定集合
使 ObservableCollection 可被远程 Javascript 客户端观察。
引言
本文介绍了一种自定义控件,该控件可以轻松地创建一个 数据绑定 的 HTML 表格 到一个 远程、服务器端的 ObservableCollection
- 服务器上使用
ObservableCollection
以 通知远程客户端 更新。 - 它使用 knockout.js 库进行 客户端数据绑定。
- 它在内部使用 websockets,但由 Spike-Engine 抽象化,对于旧浏览器将回退到 flash sockets。
- 它具有 跨平台 的特性,并且具有最小化的数据包负载和 消息压缩。
- 应用程序服务器是 自托管的可执行文件,而客户端只是一个 纯 HTML 文件。
[查看实时演示]
背景
在撰写本文前的几周,我有一个有趣的想法,我想抽象化客户端 - 服务器网络,并创建一个漂亮的 API,该 API 将维护一个 HTML 表格,该表格通过远程数据绑定到 ObservableCollection 自动填充。 本质上,允许数据绑定视图在每次发生更改以及将新项目添加到集合或从集合中删除时实时更新。 这将使人们能够无缝且轻松地构建非常动态且美观的网站,从而为用户提供出色的用户体验。 本文介绍了我设计的一种方法,该方法使用
- Knockout.js 用于客户端数据绑定
- Spike-Engine 用于客户端 - 服务器通信
为了实现这一点,我创建了一个 同步列表 的抽象,在服务器上称为 SyncList<T>
,它继承自 ObservableCollection
,以及 SyncView
,一个 JavaScript 对象,代表绑定到我们的 SyncList<T> 的视图,如下所示
使用代码
我希望 API 非常易于使用且直观。 毕竟,复杂的现代网站包含许多集合,并且设置时间应尽可能短且简单。
首先,在 服务器上,我们需要创建一个 SyncList
集合。 该集合应该足够智能,可以自行传播更改。
var list = new SyncList<TestItem>("MyList");
在我们的 HTML 页面上,我们需要首先创建一个占位符 <div>
元素,该元素将包含我们的表格。
<div data-bind='syncList: list1.gridViewModel'></div>
然后,我们需要将 <div>
元素 绑定 到远程集合,并指定列,标题和各种布局属性
var endpoint = new spike.ServerChannel("127.0.0.1:8002"); var list1 = new SyncView({ server: endpoint, name: "MyList", columns: [ { headerText: "Id", rowText: "Id" }, { headerText: "Name", rowText: "Name" }, { headerText: "Packets In", rowText: "Incoming" }, { headerText: "Packets Out", rowText: "Outgoing" }, { headerText: "Time", rowText: "Time" }, ], pageSize: 8 }); ko.applyBindings(list1);
这确实是全部,集合按名称绑定,并且此库会将服务器上集合的每个更改自动传播给我们的客户端。 由于某种原因,感觉几乎是神奇的。
服务器端 SyncList
为了节省空间,我不会在本文中显示完整的服务器端实现,但是请随意浏览代码。 但是,中心类是 SyncList<T>
,它本质上继承自 ObservableCollection
并且通过 PubHub
将事件转发给远程客户端。
/// <summary>
/// Represents an observable list which is automatically synchronized with one
/// or many remote clients.
/// </summary>
/// <typeparam name="T">The type of the element in the collection.</typeparam>
public class SyncList<T> : ObservableCollection<T>, IDisposable
{
private readonly PubHub Hub;
private readonly string Name;
/// <summary>
/// Constructs a new instance of <see cref="SyncList"/>.s
/// </summary>
/// <param name="name">The name of the collection.</param>
public SyncList(string name)
{
// Validate
if (String.IsNullOrEmpty(name))
throw new ArgumentNullException("name");
// Create a PubHub
this.Name = name;
this.Hub = Service.Hubs.GetOrCreatePubHub(this.Name);
// Hook observable collection events
this.CollectionChanged += OnCollectionChanged;
this.Hub.ClientSubscribe += OnClientSubscribe;
}
/// <summary>
/// Occurs when a new client have subscribed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="client">The client who have just subscribed.</param>
private void OnClientSubscribe(IHub sender, IClient client)
{
// Send everything
this.Hub.PublishTo(new SyncListEvent(this.Items as IList), client);
}
/// <summary>
/// Occurs when the colection is changed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Publish the event
this.Hub.Publish(new SyncListEvent(e));
}
(...)
}
此外,还有一些代码实现了 IDisposable
模式,以确保在不再需要该集合时,所有内容都已清理干净。
客户端代码
在客户端,我们使用 ko.observableArray
(http://knockoutjs.com/documentation/observableArrays.html) 并且我们相应地处理 ObservableCollection
的事件。
this._server.on('connect', function () {
self._server.hubSubscribe(self._name, null);
});
// Make sure we have created an event object
if (this._server.hubEventInform == null) {
this._server.hubEventInform = function (p) {
$.event.trigger({
type: "hubEvent",
hubName: p.hubName,
message: JSON.parse(p.message),
time: new Date()
});
};
}
// Attach a handler
$(document).on("hubEvent", function (event) {
if (self._name != event.hubName)
return;
var value = event.message;
if (value.Action == "Reset") {
self.clear();
}
if (value.Action == "Remove") {
self.removeAt(value.OldIndex);
}
if (value.Action == "Replace") {
self.replace(value.NewIndex, value.NewItems[0]);
}
if (value.Action == "Move") {
self.move(value.OldIndex, value.NewIndex);
}
if (value.Action == "Add" || value.Action == "Reset") {
if (value.NewItems != null) {
value.NewItems.forEach(function (item) {
self.add(item);
});
}
}
});
我还创建了一个客户端自定义控件,该控件的灵感来自 knockoutjs 网站上的 分页网格示例。 所有实现都附加到本文的 zip 文件中,快去看看!
历史
- 2015 年 6 月 23 日 - 源代码和文章已更新为 Spike v3.0
- 2014 年 3 月 12 日 - 源代码已更新为 Spike v2.3
- 2013 年 9 月 14 日 - 初始文章