Click here to Skip to main content
Click here to Skip to main content
Go to top

Remotely databind a Collection with Spike & Knockout.js

, 12 Mar 2014
Rate this:
Please Sign up or sign in to vote.
Making an ObservableCollection observable by remote javascript clients.

Introduction

This article presents a custom control that allows to easily create a data-bound an HTML Table to a remote, server-side ObservableCollection:

  1. An ObservableCollection is used on the server in order to notify remote clients of updates.
  2. It uses knockout.js library for client-side databinding.
  3. It uses websockets internally, but abstracted by Spike-Engine, will fallback to flash sockets for older browsers.
  4. It is cross-platform and with a minimized packet payload and message compression.
  5. The application server is a self-hosted executable and the client is just a plain HTML file.

[View a live demo]

Background

Few weeks before writing this article I had an interesting idea, I wanted to abstract client-server networking and create a nice API that would maintain an HTML Table automatically populated by being data-bound remotely on an ObservableCollection. Essentially, allowing data-bound views be updated in real-time whenever a change occurs and a new item is added or removed from the collection. This would allow people to build seamlessly and easily very dynamic and great looking websites, providing users with a great user experience. This article presents an approach I designed that uses:

  • Knockout.js for client side databinding
  • Spike-Engine for client-server communication

In order to accomplish this, I created the abstraction of a synchronized list, called SyncList<T> that inherits from ObservableCollection on the server, and a SyncView, a JavaScript object that represents the bound view to our SyncList<T>, as shown on the image below:

Using the code

I wanted the API to be very easy and intuitive to use. After all, complex modern websites contain many collections and the setup time should be minimal and simple.

First, on the server we need to create a SyncList collection. This collection should be smart enough to propagate the changes itself.

var list = new SyncList<TestItem>("MyList"); 

On our HTML page, we need to first create a placeholder <div> element which would contain our table.

<div data-bind='syncList: list1.gridViewModel'></div>

And then, we need to bind our <div> element to the remote collection, and specify the columns, titles, and various layout properties:

var endpoint = new 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); 

And this is really it, the collections are bound by name and this library will propagate automatically every change to the collection on the server to our clients. For some reason, feels almost magical.

Server-Side SyncList

For the sake of space, I won't show complete server-side implementation in the article, but feel free to explore the code. However, the central class is SyncList<T> which essentially inherits from ObservableCollection and forwards the events to the remote clients via a 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));
    }
 

    (...)
 
}

In addition, there's some more code implementing IDisposable pattern to ensure that everything is cleaned-up when the collection is no longer needed.

Client-Side Code

On the client we use a ko.observableArray ( http://knockoutjs.com/documentation/observableArrays.html ) and we handle the events of ObservableCollection accordingly.

 this._server.onConnect(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);
            });
        }
    }
}); 

I've also created a custom control on the client-side that is inspired by the paged grid sample on the knockoutjs website. All the implementation is attached in the zip file to this article, check it out!

History

  • 3/12/2014 - Source code updated to Spike v2.3
  • 9/14/2013 - Initial Article


License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Kel_
Chief Technology Officer Misakai Ltd.
Ireland Ireland
Roman Atachiants is the guy behind www.spike-engine.com project, a real-time, client-server networking layer (SOA, RPC) for .NET developers. Also the founder of Misakai Ltd..
 
He is a software engineer and scientist with extensive experience in different computer science domains, programming languages/principles/patterns & frameworks.
 
His main expertise consists of C# and .NET platform, game technologies, cloud, human-computer interaction, big data and artificial intelligence. He has an extensive programming knowledge and R&D expertise.

 
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 4 Pinmemberstefanveliki16-Sep-13 4:21 
SuggestionWhy pay for Spike Engine vs. free SignalR Pinmemberstefanveliki16-Sep-13 4:21 
GeneralRe: Why pay for Spike Engine vs. free SignalR PinmemberKel_16-Sep-13 4:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140922.1 | Last Updated 12 Mar 2014
Article Copyright 2013 by Kel_
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid