Click here to Skip to main content
Click here to Skip to main content

WebBinding - How to Bind Client JavaScript Objects to Server .NET Objects

, 1 May 2014
Rate this:
Please Sign up or sign in to vote.
In this article, I explain in a step by step manner how we can implement a binding mechanism for binding .NET server objects to JavaScript client objects and use it for creating MVVM web applications.

Table of Contents

Introduction

Single Page web Applications are very common these days. In order to simplify the binding between the view (the HTML elements) and the model (the JavaScript object), we can use one of the JavaScript libraries (e.g. Knockout, Angular, Backbone, Ember, etc.) that help us to apply that binding on our page.

Sometimes, we also want to synchronize our web pages with some changes that occur in the server side. The SignalR library enables invoking client functions from the server side (and vice versa). That can be very helpful for that purpose but, this isn't the binding I wanted.

Using WPF (with the MVVM pattern), we can reflect the changes of the business logic models to the view, by changing the corresponding values in the view-model (and, thanks to WPF binding, the changes are automatically reflected to the UI). That method works fine for Windows applications but, when dealing with web application, the picture is different. While in Windows applications, the .NET model (of the client) and the view are in the same side (the client side), in the web world, the .NET model is in the server side (the web server) and, the view is an HTML page that is presented in the client's web-browser.

Since I wanted to keep going programming as I used to in WPF (bind the view to a .NET view-model), also with web applications, I thought that it can be good if we'll have the same mechanism, for web applications too. But, let's see what we already have. We can bind JavaScript objects' properties to HTML elements (using one of the JavaScript libraries that help us to apply that binding). We can bind .NET objects' properties (dependency properties) to other .NET objects' properties (using the WPF binding). So, all what we need to complete the picture is a way to bind client's JavaScript objects to corresponding server's .NET objects.

This article shows how we can implement a mechanism for binding .NET server objects to JavaScript client objects and, apply it on HTML elements.

Background

This article assumes a familiarity with the C# language, the Javascript language and, the ASP.NET framework.

Since, we implement the dedicate part of our client script using the Knockout library (due to the generic implementation of the client script, it can be implemented with other libraries too...), a basic familiarity with that library is recommended too.

How It Works

Handle Server Bindings

Describe the binding mapping

The first step for creating our web-binding mechanism is, creating a data-structure for describing the mapping between the server object's properties and the client object's properties:

public enum BindingMappingMode
{
    None = 0,
    OneWay = 1,
    OneWayToSource = 2,
    TwoWay = 3
}

public class PropertyMapping
{
    public PropertyMapping()
    {
        MappingMode = BindingMappingMode.TwoWay;
    }

    public string ClientPropertyPath { get; set; }
    public string ServerPropertyPath { get; set; }
    public bool IsCollection { get; set; }
    public BindingMappingMode MappingMode { get; set; }
}

public class BindingMapping
{
    public BindingMapping()
    {
    }

    public BindingMapping(PropertyMapping rootPropertyMapping)
    {
        _rootMapping = rootPropertyMapping;
    }

    #region RootMapping

    private PropertyMapping _rootMapping;

    public PropertyMapping RootMapping
    {
        get { return _rootMapping ?? (_rootMapping = new PropertyMapping()); }
    }

    #endregion
}

The RootMapping property holds a mapping between a server object's property and a client object's property. The value of that property can be sufficient for describing properties with simple types (e.g. string, int, etc..) but, when dealing with more complex types, that have properties of their own, we need a way to describe the mapping of the sub-properties too. For that purpose, we add the SubPropertiesMapping and the HasSubPropertiesMapping properties:

#region SubPropertiesMapping

private List<BindingMapping> _subPropertiesMapping;

public List<BindingMapping> SubPropertiesMapping
{
    get { return _subPropertiesMapping ?? (_subPropertiesMapping = new List<BindingMapping>()); }
}

#endregion

#region HasSubPropertiesMapping

public bool HasSubPropertiesMapping
{
    get { return _subPropertiesMapping != null && _subPropertiesMapping.Count != 0; }
}

#endregion

Another case that has to be supported is collections. For cases of collections of simple types, the value of the RootMapping property can be sufficient. But, when dealing with collections of more complex types, we need a way to describe the mapping for the collection's element's type. For that purpose, we add the CollectionElementMapping and the HasCollectionElementMapping properties:

#region CollectionElementMapping

private List<BindingMapping> _collectionElementMapping;

public List<BindingMapping> CollectionElementMapping
{
    get { return _collectionElementMapping ?? (_collectionElementMapping = new List<BindingMapping>()); }
}
            
#endregion

#region HasCollectionElementMapping

public bool HasCollectionElementMapping
{
    get { return _collectionElementMapping != null && _collectionElementMapping.Count != 0; }
}

#endregion

Handle properties bindings

So, we have a data-structure for holding the binding mapping. Now, we need mechanism for applying that binding. Fortunately, we already have a same mechanism built-in in the .NET framework - the WPF Data Binding. We can use that mechanism for our purpose too.

For handling the binding of a server's object property, we add a DependencyObject and, apply WPF binding on a DependencyProperty of it:

public class PropertyBindingHandler : DependencyObject, IDisposable
{
    #region Fields

    private BindingsHandler _rootBindingsHandler;
    private BindingMapping _mapping;
    private object _rootObject;
    private Binding _propertyBinding;

    #endregion

    public PropertyBindingHandler(BindingsHandler rootBindingsHandler, BindingMapping mapping, object rootObject,
        string serverPropertyPath, string clientPropertyPath)
    {
        _rootBindingsHandler = rootBindingsHandler;
        _mapping = mapping;
        _rootObject = rootObject;
        ServerPropertyPath = serverPropertyPath;
        ClientPropertyPath = clientPropertyPath;

        _propertyBinding = new Binding
        {
            Source = rootObject,
            Path = new PropertyPath(serverPropertyPath),
            Mode = BindingMappingModeToBindingMode(mapping.RootMapping.MappingMode)
        };

        BindingOperations.SetBinding(this, CurrentValueProperty, _propertyBinding);
    }

    private BindingMode BindingMappingModeToBindingMode(BindingMappingMode bmm)
    {
        if (bmm == BindingMappingMode.TwoWay)
        {
            return BindingMode.TwoWay;
        }

        if (bmm == BindingMappingMode.OneWayToSource)
        {
            return BindingMode.OneWayToSource;
        }

        return BindingMode.OneWay;
    }

    public string ClientPropertyPath { get; set; }

    public string ServerPropertyPath { get; set; }    

    #region CurrentValue

    protected object CurrentValue
    {
        get { return GetValue(CurrentValueProperty); }
        set { SetValue(CurrentValueProperty, value); }
    }

    public static readonly DependencyProperty CurrentValueProperty =
        DependencyProperty.Register("CurrentValue", typeof(object), 
            typeof(PropertyBindingHandler), new UIPropertyMetadata(null));

    #endregion

    #region IDisposable implementation
    public void Dispose()
    {
        BindingOperations.ClearBinding(this, PropertyBindingHandler.CurrentValueProperty);
    }
    #endregion
}

In that way, we have our server's object property bound to the CurrentValue property. In addition to that, in order to be notified when the server's object property is changed, we raise an event for every change of the CurrentValue property:

public class ValueChangedEventArgs : EventArgs
{
    public object OldValue { get; set; }
    public object NewValue { get; set; }
}

public class PropertyBindingHandler : DependencyObject, IDisposable
{
    // ...

    public static readonly DependencyProperty CurrentValueProperty =
        DependencyProperty.Register("CurrentValue", typeof(object), 
            typeof(PropertyBindingHandler), new UIPropertyMetadata(null, OnCurrentValueChanged));

    private static void OnCurrentValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        PropertyBindingHandler pbh = o as PropertyBindingHandler;
        if (null == pbh)
        {
            return;
        }

        EventHandler<ValueChangedEventArgs> handler = pbh.CurrentValueChanged;
        if (null != handler)
        {
            ValueChangedEventArgs args = new ValueChangedEventArgs
            {
                OldValue = e.OldValue,
                NewValue = e.NewValue
            };

            handler(pbh, args);
        }
    }

    public event EventHandler<ValueChangedEventArgs> CurrentValueChanged;

    // ...
}

Later in this article, we'll see how we can use the CurrentValue property, for applying that binding to a client's (JavaScript) object property.

Handle objects bindings

For now, we have a class for handling the binding for a single property. The next step is to create a class for handling the binding for an entire object:

public class BindingsHandler : DependencyObject, IDisposable
{
    #region Fields

    // Dictionary of (client sub-property-path, property binding handler).
    private readonly Dictionary<string, PropertyBindingHandler> _propertiesHandlers;

    #endregion

    public BindingsHandler(object serverObject, BindingMapping bm)
    {
        _propertiesHandlers = new Dictionary<string, PropertyBindingHandler>();

        Locker = new object();

        InitializePropertiesHandlers(serverObject, bm);
    }

    #region Properties

    public string BindingId { get; set; }

    public object Locker { get; private set; }

    #endregion

    #region InitializePropertiesHandlers
    private void InitializePropertiesHandlers(object serverObject, BindingMapping bm)
    {
        AddPropertiesHandlers(serverObject, bm, string.Empty, string.Empty);
    }

    public void AddPropertiesHandlers(object serverObject, BindingMapping bm,
        string baseServerPropertyPath, string baseClientPropertyPath)
    {
        if (bm.RootMapping.MappingMode == BindingMappingMode.None)
        {
            return;
        }

        string serverPropertyPath =
            string.IsNullOrEmpty(baseServerPropertyPath)
                ? bm.RootMapping.ServerPropertyPath
                : string.IsNullOrEmpty(bm.RootMapping.ServerPropertyPath)
                      ? baseServerPropertyPath
                      : string.Format("{0}.{1}", baseServerPropertyPath, bm.RootMapping.ServerPropertyPath);

        string clientPropertyPath =
            string.IsNullOrEmpty(baseClientPropertyPath)
                ? bm.RootMapping.ClientPropertyPath
                : string.IsNullOrEmpty(bm.RootMapping.ClientPropertyPath)
                      ? baseClientPropertyPath
                      : string.Format("{0}.{1}", baseClientPropertyPath, bm.RootMapping.ClientPropertyPath);

        if (bm.HasSubPropertiesMapping)
        {
            foreach (BindingMapping subPropertyMapping in bm.SubPropertiesMapping)
            {
                AddPropertiesHandlers(serverObject, subPropertyMapping, serverPropertyPath, clientPropertyPath);
            }
        }
        else
        {
            PropertyBindingHandler pbh = null;

            if (CheckAccess())
            {
                pbh = new PropertyBindingHandler(this, bm, serverObject, serverPropertyPath, clientPropertyPath);
            }
            else
            {
                Dispatcher.Invoke(
                    new Action(
                        () =>
                        pbh =
                        new PropertyBindingHandler(this, bm, serverObject, serverPropertyPath, clientPropertyPath)));
            }

            pbh.CurrentValueChanged += OnPropertyCurrentValueChanged;

            PropertyBindingHandler oldHandler = null;

            lock (Locker)
            {
                if (_propertiesHandlers.ContainsKey(clientPropertyPath))
                {
                    oldHandler = _propertiesHandlers[clientPropertyPath];
                    _propertiesHandlers.Remove(clientPropertyPath);
                }

                _propertiesHandlers.Add(clientPropertyPath, pbh);
            }

            if (oldHandler != null)
            {
                oldHandler.Dispose();
            }
        }
    }

    void OnPropertyCurrentValueChanged(object source, ValueChangedEventArgs e)
    {
        EventHandler<PropertyValueChangedEventArgs> handler = PropertyValueChanged;
        if (handler != null)
        {
            PropertyBindingHandler pbh = source as PropertyBindingHandler;

            PropertyValueChangedEventArgs arg =
                new PropertyValueChangedEventArgs
                {
                    NewValue = e.NewValue,
                    OldValue = e.OldValue,
                    ServerPropertyPath = pbh != null ? pbh.ServerPropertyPath : string.Empty,
                    ClientPropertyPath = pbh != null ? pbh.ClientPropertyPath : string.Empty
                };

            handler(this, arg);
        }
    }

    #endregion

    public event EventHandler<PropertyValueChangedEventArgs> PropertyValueChanged;

    #region IDisposable implementation
    public void Dispose()
    {
        List<PropertyBindingHandler> propHandlers;
        lock (Locker)
        {
            propHandlers = _propertiesHandlers.Select(p => p.Value).ToList();
            _propertiesHandlers.Clear();
        }

        propHandlers.ForEach(p => p.Dispose());
    }
    #endregion
}

public class PropertyValueChangedEventArgs : ValueChangedEventArgs
{
    public string ServerPropertyPath { get; set; }
    public string ClientPropertyPath { get; set; }
}

In that class, we have a Dictionary that holds the property binding handler, for each client property (_propertiesHandlers). After we create that Dictionary, we initialize it (in the InitializePropertiesHandlers method), according to the given BindingMapping.

Note that in order to synchronize the whole of the access to the bound dependency property to one thread, the PropertyBindingHandler instances are created using the Dispatcher's thread. This will be discussed in more detail in the next section.

Handle pages bindings

The dispatcher thread

As mentioned in the previous section, we use a Dispatcher for synchronizing some of our operations. This topic is more related to WPF than the ASP.NET but, since this is a major part of our binding mechanism, I think that it is worth some words.

One of the issues that we have to deal with, when working with dependency properties (in our case, the PropertyBindingHandler.CurrentValue property), is that they can be used only from the thread that has created the dependency-object that contains them (a.k.a "the owner thread"). For solving this issue, every DispatcherObject (base class of DependencyObject) has a Dispatcher property that holds the dispatcher for the thread that has created the object. Using that dispatcher, we can invoke actions (on the appropriate thread) with dependency propeperties, also from threads that don't own the object.

In order to apply that behavior, we create a singleton class that maintains a dispatcher thread for our operations:

public class BinderContext : IDisposable
{
    #region Singleton implementation
    private BinderContext()
    {
        InitializeBinderDispatcher();
    }

    private readonly static BinderContext _instance = new BinderContext();
    public static BinderContext Instance { get { return _instance; } }
    #endregion

    #region Binder Dispatcher

    private System.Windows.Threading.Dispatcher _binderDispatcher;
    private Thread _binderDispatcherThread;

    private void InitializeBinderDispatcher()
    {
        AutoResetEvent threadCreateEvent = new AutoResetEvent(false);
        _binderDispatcherThread =
            new Thread(() =>
            {
                _binderDispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;
                threadCreateEvent.Set();
                System.Windows.Threading.Dispatcher.Run();
            });
        _binderDispatcherThread.Start();
        threadCreateEvent.WaitOne();
    }

    private void ShutdownBinderDispatcher()
    {
        if (_binderDispatcherThread != null && _binderDispatcher != null)
        {
            _binderDispatcher.InvokeShutdown();
            _binderDispatcherThread.Join();

            _binderDispatcher = null;
            _binderDispatcherThread = null;
        }
    }

    public void Invoke(Action a)
    {
        if (_binderDispatcher != null)
        {
            _binderDispatcher.Invoke(a);
        }
    }

    public void BeginInvoke(Action a)
    {
        if (_binderDispatcher != null)
        {
            _binderDispatcher.BeginInvoke(a);
        }
    }

    #endregion

    #region IDisposable implementation
    public void Dispose()
    {
        ShutdownBinderDispatcher();
    }
    #endregion
}

In that class, we run a thread that manages our dispatcher loop and, expose methods for invoking action on our dispatcher thread synchronously (Invoke) and asynchronously (BeginInvoke).

Register pages bindings

In order to register bindings for our pages, we:

  1. Add a Dictionary for holding the bindings handlers of the pages:
    // Dictionary of (page-id, (binding-id, bindings-handler)).
    private readonly Dictionary<string, Dictionary<string, BindingsHandler>> _pagesBindings;
  2. Add a method for registering a binding for a page:
    public object Locker { get; private set; }
    
    public bool RegisterBinding(string pageId, string bindingId,
        object serverObject, BindingMapping objectBindingMapping, bool overrideIfExist)
    {
        if (string.IsNullOrEmpty(pageId) ||
            string.IsNullOrEmpty(bindingId) ||
            null == serverObject ||
            null == objectBindingMapping)
        {
            return false;
        }
    
        BindingsHandler bh = null;
        Invoke(() => bh = new BindingsHandler(serverObject, objectBindingMapping));
        bh.BindingId = bindingId;
    
        lock (Locker)
        {
            _pagesLastActionTime[pageId] = DateTime.Now;
    
            Dictionary<string, BindingsHandler> pages;
            if (_pagesBindings.ContainsKey(pageId))
            {
                pages = _pagesBindings[pageId];
            }
            else
            {
                pages = new Dictionary<string, BindingsHandler>();
                _pagesBindings[pageId] = pages;
            }
    
            if (!pages.ContainsKey(bindingId) || overrideIfExist)
            {
                pages[bindingId] = bh;
            }
        }
    
        return true;
    }

Handle Changes Notifications

Client object model

The data structure

Until now, we discussed about the server's object model. Now, let's create the client-side JavaScript code. The first thing we do is create a script file (BinderClient.js) with an object (constructor function) for handling the binding in the client:

/*--- 
Since we want to remove the comments when minimizing the code,
the whole of the comments (including this one) are wrapped with a similar pattern.

Variables names can be contained in other variables names
(for example: the 'bindingObject' name is contained in the 'bindingObjectId' name).
Since when we minimizing the code we perform a search for the variables names, 
a variable name that contains the name of another variable, can lead to an unexpected result.
Therefore, in order to prevent this behavior, some of the variables' names are decorated with a '_' prefix and suffix.
---*/

function _WebBindingBinderClient_(id) {
    var self = this;

    this.pageId = id;
}

To that object, we add objects for storing the needed data:

  • An object for storing the bound objects:
    /*--- Holds the root object, for each root-object id. ---*/
    var _rootBindingObjects_ = {};
  • An object for storing the identifier of the bound object, for each binding:
    /*--- Holds the root-object id, for each binding id. ---*/
    var _bindingsObjectsIds_ = {};
  • An object for storing a creator function for each bound property:
    /*--- Holds the objects' properties' creator functions, for each root-object id. ---*/
    var _objectsCreators_ = {};
  • An object for storing the properties' names for each bound property's object (if it isn't of a simple type):
    /*--- Holds the objects' properties' names, for each root-object id. ---*/
    var _objectsPropertiesNames_ = {};

In addition to those objects, we add some functions for getting and setting their data:

function _getBindingObjectId_(_bindingId_) {
    return _bindingsObjectsIds_[_bindingId_];
}

function _getBindingObject_(_bindingId_) {
    var _rootObjId_ = _getBindingObjectId_(_bindingId_);
    var res = _rootObjId_ ? _rootBindingObjects_[_rootObjId_] : null;
    return res;
}

function _setBindingObject_(_bindingId_, rootObj) {
    var _rootObjId_ = null;
    for (var objId in _rootBindingObjects_) {
        if (_rootBindingObjects_[objId] == rootObj) {
            _rootObjId_ = objId;
        }
    }

    if (!_rootObjId_) {
        _rootObjId_ = _bindingId_ + "O";
        _rootBindingObjects_[_rootObjId_] = rootObj;
    }

    _bindingsObjectsIds_[_bindingId_] = _rootObjId_;

    return _rootObjId_;
}

function _getBindingObjectsCreators_(_rootObjId_) {
    return _objectsCreators_[_rootObjId_];
}

function _setObjectCreator_(_rootObjId_, _propId_, objCreator) {
    var bindingObjectsCreators = _retrieveObjectProperty_(_objectsCreators_, _rootObjId_, {});
    bindingObjectsCreators[_propId_] = objCreator;
}

function _getObjectCreator_(_rootObjId_, _propId_) {
    var res;

    var bindingObjectsCreators = _getBindingObjectsCreators_(_rootObjId_);

    if (_propId_ && bindingObjectsCreators && bindingObjectsCreators[_propId_]) {
        res = bindingObjectsCreators[_propId_];
    } else {
        res = _createEmptyObject_;
    }

    return res;
}

function _getObjectPropertiesNames_(_rootObjId_, _propId_) {
    var bindingObjectsPropertiesNames = _objectsPropertiesNames_[_rootObjId_];
    return bindingObjectsPropertiesNames ? bindingObjectsPropertiesNames[_propId_] : null;
}

function _setObjectPropertiesNames_(_rootObjId_, _propId_, objProperties) {
    var bindingObjectsPropertiesNames = _retrieveObjectProperty_(_objectsPropertiesNames_, _rootObjId_, {});
    bindingObjectsPropertiesNames[_propId_] = objProperties;
}

function _retrieveObjectProperty_(obj, propName, defualtValue) {
    var p = obj[propName];
    if (!p) {
        p = defualtValue;
        obj[propName] = p;
    }

    return p;
}
Generic implementation

As mentioned before, our solution is for binding server's .NET objects to client's JavaScript objects. For binding JavaScript objects to HTML DOM elements, we have other JavaScript libraries (like Knockout, etc..) that can be used. In order to make our solution compatible with any JavaScript library, we separate the implementation of the dedicate (for a specific library) code. For that purpose, we add variables for holding the dedicate functions:

  • Functions for creating an object holder (a wrapper object for a specific library):
    this.createObjectHolder = function() { return {}; };
    this.createArrayHolder = function() { return []; };
  • Functions for getting or setting an object from an object holder:
    this.getObjectValue = function(objHolder) { return {}; };
    this.getArrayValue = function(arrHolder) { return []; };
    this.setObjectValue = function(objHolder, val) {};
    this.setArrayValue = function(arrHolder, val) {};
  • Functions for registering for changes notifications:
    this.registerForPropertyChanges = function(objHolder, propNotificationFunc) {};
    this.registerForArrayChanges = function(arrHolder, arrNotificationFunc) {};

For implementing those functions using the knockout library, we add a script file (KnockoutDedicateImplementation.js) with the dedicate implementation:

function WebBinding_ApplyKnockoutDedicateImplementation(wbObj) {
    wbObj.getObjectValue = function (objHolder) {
        return objHolder();
    };

    wbObj.getArrayValue = function (arrHolder) {
        return arrHolder();
    };

    wbObj.setObjectValue = function (objHolder, val) {
        objHolder(val);
    };

    wbObj.setArrayValue = function (arrHolder, val) {
        arrHolder(val);
    };

    wbObj.createObjectHolder = function () {
        return ko.observable();
    };

    wbObj.createArrayHolder = function () {
        return ko.observableArray([]);
    };

    wbObj.registerForPropertyChanges = function (objHolder, propNotificationFunc) {
        objHolder.subscribe(function (newValue) {
            propNotificationFunc();
        });
    };

    wbObj.registerForArrayChanges = function (arrHolder, arrNotificationFunc) {
        arrHolder.subscribe(function (changes) {
            arrNotificationFunc();
        }, null, "arrayChange");
    }; 
}

This is the only dedicate code that we have to write. The whole of the other implementation is with standard JavaScript only. Later, we'll see how the things are joined together.

Construct the client model

For creating objects for the bound properties according to the binding-mapping, we add a function that uses the appropriate stored creator function:

function _createObject_(_rootObjId_, _propId_) {
    var creatorFunc = _getObjectCreator_(_rootObjId_, _propId_);
    var res = creatorFunc();

    return res;
}

For setting the appropriate creator functions, we add:

  1. A function for setting a creator function for a non-array property:
    function _addCreatorForObjectId_(_rootObjId_, _propId_, objPropNames) {
        /*--- We create a constructor function only for the root property - 
        the sub-properties will be created by the constructor function ---*/
    
        /*--- There can be another binding that uses the same property.
        So, get the exist creator function. ---*/
        var initialObjectCreator = _getObjectCreator_(_rootObjId_, _propId_);
            
        if (objPropNames.length > 0) {
            var objCreator = function () {
                var objHolder = initialObjectCreator();
                var obj = self.getObjectValue(objHolder);
                for (var propInx = 0; propInx < objPropNames.length; propInx++) {
                    var currPropName = objPropNames[propInx];
                    var currPropId = _propId_ + "." + currPropName;
                    obj[currPropName] = _createObject_(_rootObjId_, currPropId);
                }
    
                return objHolder;
            };
    
            _setObjectCreator_(_rootObjId_, _propId_, objCreator);
        } else {
            /*--- If there is no property paths for the array's element
            it is a simple type. Just return an empty object. ---*/
            _setObjectCreator_(_rootObjId_, _propId_, initialObjectCreator);
        }
    }
  2. A function for setting a creator function for an array property:
    function _addCreatorsForArrayId_(_rootObjId_, _propId_, elementPropNames, isArrayOfArray) {
        /*--- Add creator for the array ---*/
        _setObjectCreator_(_rootObjId_, _propId_, _createEmptyArray_);
    
        /*--- If the element of the array is array itself, 
                its creator will be added by another call... ---*/
        if (!isArrayOfArray) {
            /*--- Add creator for the array's element ---*/
            var arrayElementId = _propId_ + "[]";
            _addCreatorForObjectId_(_rootObjId_, arrayElementId, elementPropNames);
        }
    }
    
    function _createEmptyArray_() {
        var arrHolder = self.createArrayHolder();
        self.setArrayValue(arrHolder, []);
    
        return arrHolder;
    }

For setting the appropriate data for each binding-mapping, we:

  1. Add a method for generating a client object string from a BindingMapping:
    public string ToClientBindingMappingObjectString()
    {
        StringBuilder sb = new StringBuilder();
    
        AppendClientBindingMappingObjectString(sb);
    
        return sb.ToString();
    }
    
    private void AppendClientBindingMappingObjectString(StringBuilder sb)
    {
        string clientPropPath = RootMapping.ClientPropertyPath ?? string.Empty;
        string[] clientPathParts = clientPropPath.Split('.');
        if (clientPropPath.Length > 1)
        {
            // Create binding-mapping with the separated client property-path.
            bool isLowerPart = true;
            BindingMapping separatedMapping = null;
            for (int partInx = clientPathParts.Length - 1; partInx >= 0; partInx--)
            {
                BindingMapping currMapping;
                if (isLowerPart)
                {
                    currMapping = FromPropertyMapping(clientPathParts[partInx], RootMapping.ServerPropertyPath, 
                        RootMapping.MappingMode);
    
                    currMapping.RootMapping.IsCollection = RootMapping.IsCollection;
    
                    if (HasSubPropertiesMapping)
                    {
                        SubPropertiesMapping.ForEach(p => currMapping.SubPropertiesMapping.Add(p));
                    }
    
                    if (HasCollectionElementMapping)
                    {
                        CollectionElementMapping.ForEach(p => currMapping.CollectionElementMapping.Add(p));
                    }
    
                    isLowerPart = false;
                }
                else
                {
                    currMapping = FromPropertyMapping(clientPathParts[partInx], string.Empty, RootMapping.MappingMode);
                    currMapping.SubPropertiesMapping.Add(separatedMapping);
                }
    
                separatedMapping = currMapping;
            }
    
            if (separatedMapping != null)
            {
                separatedMapping.AppendSimpleClientBindingMappingObjectString(sb);
            }
        }
        else
        {
            AppendSimpleClientBindingMappingObjectString(sb);
        }
    }
    
    private void AppendSimpleClientBindingMappingObjectString(StringBuilder sb)
    {
        // In order to minimize the client's object string, 
        // we minimize the properties' names:
        // PP - PropertyPath
        // SPM - SubPropertiesMapping
        // CEM - CollectionElementMapping
        // IC - IsCollection
    
        sb.Append("{PP:\"");
        sb.Append(RootMapping.ClientPropertyPath);
        sb.Append("\",SPM:[");
    
        if (HasSubPropertiesMapping)
        {
            bool isFirstSubProperty = true;
            foreach (BindingMapping subPropertyMapping in SubPropertiesMapping)
            {
                if (isFirstSubProperty)
                {
                    isFirstSubProperty = false;
                }
                else
                {
                    sb.Append(',');
                }
    
                subPropertyMapping.AppendClientBindingMappingObjectString(sb);
            }
        }
    
        sb.Append("],CEM:[");
    
        if (HasCollectionElementMapping)
        {
            bool isFirstElementProperty = true;
            foreach (BindingMapping elemMapping in CollectionElementMapping)
            {
                if (isFirstElementProperty)
                {
                    isFirstElementProperty = false;
                }
                else
                {
                    sb.Append(',');
                }
    
                elemMapping.AppendClientBindingMappingObjectString(sb);
            }
        }
    
        sb.Append("],IC:");
        sb.Append(RootMapping.IsCollection ? "true" : "false");
        sb.Append('}');
    }
    
    public static BindingMapping FromPropertyMapping(string clientPropertyName, string serverPropertyPath,
        BindingMappingMode mappingMode = BindingMappingMode.TwoWay)
    {
        BindingMapping bm = new BindingMapping();
        bm.RootMapping.ClientPropertyPath = clientPropertyName;
        bm.RootMapping.ServerPropertyPath = serverPropertyPath;
        bm.RootMapping.MappingMode = mappingMode;
    
        return bm;
    }
  2. Add a function for updating the client model, according to a client object string:
    this.addBindingMapping = function (_bindingId_, rootObj, bindingMappingObj) {
        /*--- Add binding object mapping. ---*/
        var _rootObjId_ = _setBindingObject_(_bindingId_, rootObj);
    
        /*--- Add objects' creators functions and, properties arrays. ---*/
        _addObjectsCreatorsAndProperties_(_rootObjId_, bindingMappingObj);
    };
    
    function _addObjectsCreatorsAndProperties_(_rootObjId_, bindingMappingObj) {
        _addSubObjectsCreatorsAndProperties_(_rootObjId_, "", bindingMappingObj);
    }
    
    function _addSubObjectsCreatorsAndProperties_(_rootObjId_, basePropId, subBindingMappingObj) {
    
        /*---  In order to minimize the client's object string, the properties' names are minimized:
                PP - PropertyPath
                SPM - SubPropertiesMapping
                CEM - CollectionElementMapping
                IC - IsCollection
        ---*/
            
        var _propId_ = basePropId;
        if (_propId_ != "" && subBindingMappingObj.PP != "") {
            _propId_ += ".";
        }
        _propId_ += subBindingMappingObj.PP;
    
        if (subBindingMappingObj.IC) {
            /*--- It is an array ---*/
    
            var colElemMapping = subBindingMappingObj.CEM;
            var isArrayOfArray = colElemMapping.length == 1 && colElemMapping[0].IC;
                
            var elemPropNames = [];
    
            for (var elemPropInx = 0; elemPropInx < colElemMapping.length; elemPropInx++) {
                var currElemMapping = colElemMapping[elemPropInx];
                var currElemProp = currElemMapping.PP;
                if (currElemProp != "") {
                    elemPropNames.push(currElemProp);
                }
    
                _addSubObjectsCreatorsAndProperties_(_rootObjId_, _propId_ + "[]", currElemMapping);
            }
    
            _addCreatorsForArrayId_(_rootObjId_, _propId_, elemPropNames, isArrayOfArray);
            _addObjectPropertiesNames_(_rootObjId_, _propId_ + "[]", elemPropNames);
                
        } else {
            /*--- It isn't an array ---*/
    
            var subPropMapping = subBindingMappingObj.SPM;
    
            var subPropNames = [];
                
            for (var subPropInx = 0; subPropInx < subPropMapping.length; subPropInx++) {
                var currSubMapping = subPropMapping[subPropInx];
                var currSubProp = currSubMapping.PP;
                if (currSubProp != "") {
                    subPropNames.push(currSubProp);
                }
    
                _addSubObjectsCreatorsAndProperties_(_rootObjId_, _propId_, currSubMapping);
            }
    
            _addCreatorForObjectId_(_rootObjId_, _propId_, subPropNames);
            _addObjectPropertiesNames_(_rootObjId_, _propId_, subPropNames);
        }
    }
    
    function _addObjectPropertiesNames_(_rootObjId_, _propId_, subPropNames) {
        var currPropNames = _getObjectPropertiesNames_(_rootObjId_, _propId_);
        if (currPropNames && currPropNames.length > 0) {
            for (var newPropInx = 0; newPropInx < subPropNames.length; newPropInx++) {
                var newPropName = subPropNames[newPropInx];
                var isPropAlreadyExist = false;
                for (var oldPropInx = 0; oldPropInx < currPropNames.length; oldPropInx++) {
                    if (newPropName == currPropNames[oldPropInx]) {
                        isPropAlreadyExist = true;
                    }
                }
                    
                if (!isPropAlreadyExist) {
                    currPropNames.push(newPropName);
                }
            }
        } else {
            _setObjectPropertiesNames_(_rootObjId_, _propId_, subPropNames);
        }
    }

Handle data requests

For sending requests to the server from the client, we add a function that sends an AJAX POST request:

function _sendAjaxPost_(postData, handleResponseFunc) {
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState == 4) {
            if (handleResponseFunc) {
                handleResponseFunc(xmlhttp);
            }
        }
    };

    var url = "/webBindingHandler";
    xmlhttp.open("POST", url, true);
    xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xmlhttp.send(postData);
}

For routing the binder client requests via the ASP.NET Routing, to be processed by a custom HTTP handler, we create an HTTP handler for handling the binder requests and an HTTP router for routing to that HTTP handler:

internal class BinderHttpHandler : IHttpHandler
{
    #region IHttpHandler implementation

    public bool IsReusable
    {
        get { return false; }
    }

    public void ProcessRequest(HttpContext context)
    {
    }

    #endregion
}

internal class BinderRouteHandler : IRouteHandler
{
    public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new BinderHttpHandler();
    }
}

After we created the HTTP router, we have to register our routing. We can do that by adding a new Route to the RouteTable. We can achieve that goal by adding the registration code to the Application_Start method in the Global.asax.cs file (or other place in the application's startup). Using that way, we have to add our registration code for any application that uses our technology. In our case, in order to save us from that task, we take a different approach and, register it automatically at the first time we register a page's binding:

public class BinderContext : IDisposable
{
    // ...

    private static bool _isBinderHandlerRegistered = false;

    public bool RegisterBinding(string pageId, string bindingId,
                                object serverObject, BindingMapping objectBindingMapping, bool overrideIfExist)
    {
        // ...

        // Register the HTTP route handler.
        if (!_isBinderHandlerRegistered)
        {
            var binderRoute = new Route("webBindingHandler", new BinderRouteHandler());
            RouteTable.Routes.Insert(0, binderRoute);

            _isBinderHandlerRegistered = true;
        }

        // ...
    }

    // ...
}

Later, we'll implement our HTTP handler to process our binder client requests.

Notify about client properties changes

Send notification on properties changes

In order to notify the server about properties' changes in the client side, we have to send a notification message that contains the relevant changes. That can be done as follows:

/*--- Holds the properties' paths of the properties that have been changed, for each binding id. ---*/
var _pendingChangedPropertiesPathes_ = {};

function _sendPropertiesChangeNotification_() {
    var propertiesNotification = "[";

    var hasNotifications = false;

    var isFirstBindingId = true;
    for (var _bindingId_ in _pendingChangedPropertiesPathes_) {
        var pendingProperties = _pendingChangedPropertiesPathes_[_bindingId_];

        if (pendingProperties && pendingProperties.length > 0) {
            if (isFirstBindingId) {
                isFirstBindingId = false;
            } else {
                propertiesNotification += ",";
            }

            hasNotifications = true;

            propertiesNotification += '{"BindingId":"' + _bindingId_ + '","PropertiesNotifications":[';

            var isFirstPropPath = true;

            while (pendingProperties.length > 0) {
                if (isFirstPropPath) {
                    isFirstPropPath = false;
                } else {
                    propertiesNotification += ",";
                }

                var _propPath_ = pendingProperties.pop();
                propertiesNotification += _createPropertyPathNotification_(_bindingId_, _propPath_);
            }

            propertiesNotification += ']}';
        }
    }

    propertiesNotification += "]";

    if (hasNotifications) {
        var postData = "requestId=propertiesChangedNotification" +
            "&pageId=" + self.pageId + "&propertiesNotification=" + propertiesNotification;

        _sendAjaxPost_(postData, function (xmlhttp) { });
    }
}

function _createPropertyPathNotification_(_bindingId_, _propPath_) {
    var objHolder = _getPropertyObject_(_bindingId_, _propPath_);
    var propVal = objHolder ? self.getObjectValue(objHolder) : null;

    var res = '{"PropertyPath":"' + _propPath_ + '", "NewValue":"' + propVal + '"}';

    return res;
}

In the _createPropertyPathNotification_ function, we build a JSON string that contains the property's property-path and the property's new value.

In the _sendPropertiesChangeNotification_ function, we build a notification message that contains the whole of the pending (changed but, a notification hasn't been sent yet) properties' changes and, send an AJAX request with that message.

Handle special characters

As some of you probably have noticed, we send our AJAX requests' content, using the url format. Using the url format, there are some characters that have a special meaning (like &, %, etc...). In order to send those characters as a plain text, they should be encoded. In addition to that, since we use the JSON format, some characters (like \, ", etc...) have to be changed too. Therefore, in order to send the properties' values properly, some of their characters have to be encoded. That can be done as follows:

function _NoteReplacement_(orgNote, replacementString) {
    this.orgNote = orgNote;
    this.replacementString = replacementString;
}

/*--- Initialize special notes encoding, for notifications messages. ---*/
var _notesReplacementsForSend_ = [];
_notesReplacementsForSend_.push(new _NoteReplacement_(' ', '%20'));
_notesReplacementsForSend_.push(new _NoteReplacement_('`', '%60'));
_notesReplacementsForSend_.push(new _NoteReplacement_('~', '%7E'));
_notesReplacementsForSend_.push(new _NoteReplacement_('!', '%21'));
_notesReplacementsForSend_.push(new _NoteReplacement_('@', '%40'));
_notesReplacementsForSend_.push(new _NoteReplacement_('#', '%23'));
_notesReplacementsForSend_.push(new _NoteReplacement_('$', '%24'));
_notesReplacementsForSend_.push(new _NoteReplacement_('%', '%25'));
_notesReplacementsForSend_.push(new _NoteReplacement_('^', '%5E'));
_notesReplacementsForSend_.push(new _NoteReplacement_('&', '%2F%2D%26%2D%2F'));
_notesReplacementsForSend_.push(new _NoteReplacement_('*', '%2A'));
_notesReplacementsForSend_.push(new _NoteReplacement_('(', '%28'));
_notesReplacementsForSend_.push(new _NoteReplacement_(')', '%29'));
_notesReplacementsForSend_.push(new _NoteReplacement_('_', '%5F'));
_notesReplacementsForSend_.push(new _NoteReplacement_('-', '%2D'));
_notesReplacementsForSend_.push(new _NoteReplacement_('+', '%2B'));
_notesReplacementsForSend_.push(new _NoteReplacement_('=', '%3D'));
_notesReplacementsForSend_.push(new _NoteReplacement_('[', '%5B'));
_notesReplacementsForSend_.push(new _NoteReplacement_(']', '%5D'));
_notesReplacementsForSend_.push(new _NoteReplacement_('{', '%7B'));
_notesReplacementsForSend_.push(new _NoteReplacement_('}', '%7D'));
_notesReplacementsForSend_.push(new _NoteReplacement_('|', '%7C'));
_notesReplacementsForSend_.push(new _NoteReplacement_('\\', '%5C%5C'));
_notesReplacementsForSend_.push(new _NoteReplacement_('"', '%5C%22'));
_notesReplacementsForSend_.push(new _NoteReplacement_('\'', '%27'));
_notesReplacementsForSend_.push(new _NoteReplacement_(':', '%3A'));
_notesReplacementsForSend_.push(new _NoteReplacement_(';', '%3B'));
_notesReplacementsForSend_.push(new _NoteReplacement_('?', '%3F'));
_notesReplacementsForSend_.push(new _NoteReplacement_('/', '%2F'));
_notesReplacementsForSend_.push(new _NoteReplacement_('>', '%3E'));
_notesReplacementsForSend_.push(new _NoteReplacement_('.', '%2E'));
_notesReplacementsForSend_.push(new _NoteReplacement_('<', '%3C'));
_notesReplacementsForSend_.push(new _NoteReplacement_(',', '%2C'));

function _getFixedValueForSend_(orgVal) {
    var orgValStr = orgVal ? orgVal.toString() : "";

    if (orgValStr === '[object Object]' && orgVal !== '[object Object]') {
        /*--- This is an object... ---*/
        orgValStr = "";
    }

    var newValStr = "";
        
    for (var noteInx = 0; noteInx < orgValStr.length; noteInx++) {
        var currNote = orgValStr.charAt(noteInx);

        var isReplaced = false;
        for (var replacementInx = 0; replacementInx < _notesReplacementsForSend_.length && !isReplaced; replacementInx++) {
            var currReplacement = _notesReplacementsForSend_[replacementInx];
            if (currNote == currReplacement.orgNote) {
                newValStr += currReplacement.replacementString;
                isReplaced = true;
            }
        }
            
        if (!isReplaced) {
            newValStr += currNote;
        }
    }

    return newValStr;
}

function _createPropertyPathNotification_(_bindingId_, _propPath_) {
    var objHolder = _getPropertyObject_(_bindingId_, _propPath_);
    var propVal = objHolder ? self.getObjectValue(objHolder) : null;
    var fixedVal = _getFixedValueForSend_(propVal);

    var res = '{"PropertyPath":"' + _propPath_ + '", "NewValue":"' + fixedVal + '"}';

    return res;
}

The _notesReplacementsForSend_ object contains the special characters and the replacement strings for them. For the '&' note, we set a wrapped string ('/-&-/') - we convert this string back in the server side.

In the _getFixedValueForSend_ function, we convert a given value to an encoded string.

In the _createPropertyPathNotification_ function, we use the _getFixedValueForSend_ function for converting the property's value.

Process properties changes request

As mentioned before, we have a bound dependency property, for each server's property (the PropertyBindingHandler.CurrentValue property). So, in order to apply a new value for a server's property, we can just change its bound dependency property:

public class PropertyBindingHandler : DependencyObject, IDisposable
{
    // ...

    public void SetCurrentValue(object value)
    {
        object currValue = GetCurrentValue();

        if (currValue == value)
        {
            return;
        }

        if (CheckAccess())
        {
            CurrentValue = value;
        }
        else
        {
            Dispatcher.Invoke(new Action(() => CurrentValue = value));
        }
    }

    // ...
}

public class BindingsHandler : DependencyObject, IDisposable
{
    // ...

    public void SetCurrentValue(string clientPropertyPath, object value)
    {
        PropertyBindingHandler pbh = GetPropertyBindingHandler(clientPropertyPath);
        if (pbh != null)
        {
            pbh.SetCurrentValue(value);
        }
    }

    public PropertyBindingHandler GetPropertyBindingHandler(string clientPropertyPath)
    {
        PropertyBindingHandler res = null;

        lock (Locker)
        {
            if (_propertiesHandlers.ContainsKey(clientPropertyPath))
            {
                res = _propertiesHandlers[clientPropertyPath];
            }
        }

        return res;
    }

    // ...
}

Since a dependency property can be changed only from its owner thread, we use the Dispatcher for setting its value.

For getting the request's content, we create an equivalent data-structure (the same structure as the JSON string of the propertiesNotification request's field):

public class PropertiesValuesNotificationData
{
    public string BindingId { get; set; }
    public List<PropertyValueData> PropertiesNotifications { get; set; }
}

public class PropertyValueData
{
    public string PropertyPath { get; set; }

    #region NewValue

    private string _newValue;

    public string NewValue
    {
        get { return _newValue; }
        set
        {
            // Remove the wrapping for the '&' characters.
            _newValue = Regex.Replace(value, "/-&-/", "&");
        }
    }

    #endregion
}

Using that data-structure, we can handle the properties-notification request to apply the new properties' values, as follows:

internal class BinderHttpHandler : IHttpHandler
{
    // ...

    public void ProcessRequest(HttpContext context)
    {
        string requestId = context.Request.Params["requestId"];

        switch (requestId)
        {
            case "propertiesChangedNotification":
                {
                    HandleClientPropertiesChangedNotificationRequest(context);
                    break;
                }
        }
    }

    // ...

    protected bool HandleClientPropertiesChangedNotificationRequest(HttpContext context)
    {
        string pageId = context.Request.Params["pageId"];
        string propertiesNotificationStr = context.Request.Params["propertiesNotification"];

        JavaScriptSerializer jss = new JavaScriptSerializer();
        PropertiesValuesNotificationData[] propertiesNotification =
            jss.Deserialize<PropertiesValuesNotificationData[]>(propertiesNotificationStr);

        BinderContext.Instance.PostPropertiesUpdate(pageId, propertiesNotification);

        return true;
    }
}

public class BinderContext : IDisposable
{
    public BindingsHandler GetBindingsHandler(string pageId, string bindingId)
    {
        if (string.IsNullOrEmpty(pageId) ||
            string.IsNullOrEmpty(bindingId))
        {
            return null;
        }

        BindingsHandler res = null;

        lock (Locker)
        {
            if (_pagesBindings.ContainsKey(pageId))
            {
                Dictionary<string, BindingsHandler> pages =
                    _pagesBindings[pageId];

                if (pages.ContainsKey(bindingId))
                {
                    res = pages[bindingId];
                }
            }
        }

        return res;
    }

    public void UpdateProperties(string pageId, PropertiesValuesNotificationData[] propertiesNotification)
    {
        foreach (PropertiesValuesNotificationData pn in propertiesNotification)
        {
            BindingsHandler bh = GetBindingsHandler(pageId, pn.BindingId);
            if (bh != null)
            {
                foreach (PropertyValueData pv in pn.PropertiesNotifications)
                {
                    bh.SetCurrentValue(pv.PropertyPath, pv.NewValue);
                }
            }
        }
    }

    public void PostPropertiesUpdate(string pageId, PropertiesValuesNotificationData[] propertiesNotification)
    {
        BeginInvoke(() => UpdateProperties(pageId, propertiesNotification));
    }
}

In the UpdateProperties method, we get the appropriate BindingsHandler for each binding-id and, set the new values of the changed properties.

In the HandleClientPropertiesChangedNotificationRequest method, we create a collection of PropertiesValuesNotificationData objects from the given request's propertiesNotification parameter and, run the UpdateProperties method asynchronously with it. In order to prevent cases that the code of a later request will run before the code of an earlier request (due to threads race condition), we synchronize the whole of the update operations to the dispatcher thread (by using the BeginInvoke method).

Notify about server properties changes

Request server changes notifications

In order to apply server's properties' changes on the client's properties, we need a notification for every property's change. We can achieve that goal by sending an Ajax request and, getting the needed notifications in its response. That can be done as follows:

this.lastChangesResult = "[]";

function _requestServerChanges_() {
    var postData = "requestId=propertyChangeRequest" +
        "&pageId=" + self.pageId +
            "&lastChangesResult=" + self.lastChangesResult;

    _sendAjaxPost_(postData, _handleServerChangesResult_);
}

function _handleServerChangesResult_(xmlhttp) {
    if (xmlhttp.status == 200) {
        _applyServerChangesResponse_(xmlhttp.responseText);
    }

    setTimeout(function() {
        _requestServerChanges_();
    }, 0);
}

function _applyServerChangesResponse_(response) {
    var parsedResponse = eval("(" + response + ")");

    for (var elementInx = 0; elementInx < parsedResponse.length; elementInx++) {
        var currElement = parsedResponse[elementInx];

        var currProperties = currElement ? currElement.PropertiesValues : null;

        if (currProperties) {
            for (var propertyResultInx = 0; propertyResultInx < currProperties.length; propertyResultInx++) {
                var currPropertyResult = currProperties[propertyResultInx];

                _applyServerPropertyChange_(currElement.BindingId, currPropertyResult.SubPropertyPath, currPropertyResult.NewValue);
            }
        }
    }

    self.lastChangesResult = response;
}

function _applyServerPropertyChange_(_bindingId_, _propPath_, newValue) {
    var propObject = _getPropertyObject_(_bindingId_, _propPath_);
    if (propObject) {
        self.setObjectValue(propObject, newValue);
    }
}

function _getPropertyObject_(_bindingId_, _propPath_) {
    var propObject = null;
    var currVal = _getBindingObject_(_bindingId_);

    var propPathExt = _propPath_;

    while (propPathExt.length > 0) {
        var firstDotIndex = propPathExt.indexOf(".");
        var currPathPart;

        if (firstDotIndex > 0) {
            currPathPart = propPathExt.substr(0, firstDotIndex);
            propPathExt = propPathExt.substr(firstDotIndex + 1);
        } else {
            currPathPart = propPathExt;
            propPathExt = "";
        }

        propObject = _getSubPropertyObject_(currVal, currPathPart);
        currVal = propObject ? self.getObjectValue(propObject) : null;
    }

    return propObject;
}

function _getSubPropertyObject_(obj, subPropPath) {
    var propObject = null;

    if (obj && subPropPath && subPropPath.length > 0) {
        var propNamePart;
        var arrayIndicesPart;
        var firstBracketIndex = subPropPath.indexOf("[");
        if (firstBracketIndex > 0) {
            propNamePart = subPropPath.substr(0, firstBracketIndex);
            arrayIndicesPart = subPropPath.substr(firstBracketIndex);
        } else {
            propNamePart = subPropPath;
            arrayIndicesPart = "";
        }

        propObject = obj[propNamePart];

        while (arrayIndicesPart.length > 0) {
            var firstCloseBracketIndex = arrayIndicesPart.indexOf("]");
            var currIndexStr;

            if (firstCloseBracketIndex > 0) {
                currIndexStr = arrayIndicesPart.substr(1, firstCloseBracketIndex - 1);
                arrayIndicesPart = arrayIndicesPart.substr(firstCloseBracketIndex + 1);
            } else {
                currIndexStr = arrayIndicesPart.substr(1, arrayIndicesPart.length - 2);
                arrayIndicesPart = "";
            }

            var currIndex = parseInt(currIndexStr);

            var arrVal = propObject ? self.getArrayValue(propObject) : null;
                
            if (arrVal && arrVal.length > currIndex) {
                propObject = arrVal[currIndex];
            } else {
                propObject = null;
            }
        }
    }

    return propObject;
}

In the _getPropertyObject_ function, we get a property according to a given property-path.

In the _requestServerChanges_ function, we send an AJAX request for server properties changes.

In the _handleServerChangesResult_ we apply the properties changes according to the response and, request for another changes.

In each changes request, we also send the previous changes response. In this way, we can update the server on the client's properties' state.

Get server changes notification

In order to create a notification about the changed values, we have to know which values have been changed (aren't synchronized with the client). For that purpose, for each property, we add counters for indicating a difference between the client and server (if the values are different, the properties aren't synchronized):

public uint ChangeCounter { get; set; }
public uint ClientChangeCounter { get; set; }
public bool HasUpdatedValue { get { return ChangeCounter != ClientChangeCounter; } }

Using those counters, we can generate the changes response:

public class ServerPropertyValueRespose
{
    public string SubPropertyPath { get; set; }
    public string NewValue { get; set; }
    public uint ChangeCounter { get; set; }
}

public class PropertyBindingHandler : DependencyObject, IDisposable
{
    // ...

    public ServerPropertyValueRespose GetUpdatedValueResponse()
    {
        // If this is a collection, the NewValue is the new collection's elements count,
        // otherwise, the NewValue is the current binded value.
        return new ServerPropertyValueRespose
        {
            SubPropertyPath = ClientPropertyPath,
            ChangeCounter = ChangeCounter,
            NewValue = GetCurrentValueString()
        };
    }

    public string GetCurrentValueString()
    {
        object val = GetCurrentValue();

        return val != null ? val.ToString() : string.Empty;
    }

    // ...
}

public class BindingsHandler : DependencyObject, IDisposable
{
    // ...

    public List<ServerPropertyValueRespose> GetUpdatedValuesResponses()
    {
        List<PropertyBindingHandler> changedProperties;

        lock (Locker)
        {
            // Since we want the iteration on '_propertiesHandlers' to be performed inside the 'lock',
            // we convert the query's result to a list. If we don't do it,
            // because of the LINQ's differed-execution, the iteration will occur outside of the 'lock'.

            changedProperties =
                _propertiesHandlers.Where(p => p.Value.HasUpdatedValue).OrderBy(p => p.Key).Select(p => p.Value).
                    ToList();
        }

        // When we get the value of the 'PropertyBindingHandler.CurrentValue' property,
        // we wait to the action to be performed on the dispatcer's thread.
        // If we do it inside the 'lock' and, there is another operation on the dispatcher's thread,
        // that is waiting for locking the same object, we can encounter a deadlock.
        // Therefore, we get the updated values list, outside of the 'lock'.

        List<ServerPropertyValueRespose> res = changedProperties.Select(p => p.GetUpdatedValueResponse()).ToList();

        return res;
    }

    // ...
}

For indicating a property's change, we increase the ChangeCounter, for each time the property is changed:

private static void OnCurrentValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    // ...

    pbh.ChangeCounter++;

    // ...
}
Asynchronous HTTP handler

In order to notify the client about server properties' changes, we have to send a response message when there are properties that have been changed. If there are changes when we get the client's request, we can immediately send those changes back. But, if there are no changes at that point, we have to find a way for sending the changes back, when they exist.

One way for handling that is, to always send a response (even if it is empty). That way isn't so good, because it overloads the network with a lot of unnecessary (most of the responses are empty) traffic.

Another way for handing that is to wait synchronously until the changes exist and, then complete the request. That way isn't so good either, because the request can take a lot of time and suspend other requests.

So, we want to wait until there are properties' changes but, in the meantime, we want to free ASP.NET to handle other requests. For that purpose, we create an asynchronous HTTP handler:

internal class BinderHttpAsyncHandler : IHttpAsyncHandler
{
    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        ChangeRequestAsyncResult res = new ChangeRequestAsyncResult(cb, context, extraData);

        res.BeginProcess();

        return res;
    }

    public void EndProcessRequest(IAsyncResult result)
    {
    }

    public bool IsReusable
    {
        get { return false; }
    }

    public void ProcessRequest(HttpContext context)
    {
    }
}

internal class ChangeRequestAsyncResult : IAsyncResult
{
    private bool _isCompleted;
    private bool _isCompletedSynchronously;
    private Object _state;
    private AsyncCallback _callback;
    private HttpContext _context;

    public ChangeRequestAsyncResult(AsyncCallback callback, HttpContext context, Object state)
    {
        _callback = callback;
        _context = context;
        _state = state;
        _isCompleted = false;
        _isCompletedSynchronously = false;
    }

    #region IAsyncResult implementation
    public object AsyncState
    {
        get { return _state; }
    }

    public System.Threading.WaitHandle AsyncWaitHandle
    {
        get { return null; }
    }

    public bool CompletedSynchronously
    {
        get { return _isCompletedSynchronously; }
    }

    public bool IsCompleted
    {
        get { return _isCompleted; }
    }
    #endregion

    public void BeginProcess()
    {
    }
}

In that asynchronous HTTP handler, we begin an asynchronous operation, for handling the changes request. This asynchronous operation is implemented as follows:

internal class ChangeRequestAsyncResult : IAsyncResult
{
    // ...

    private List<BindingsHandler> _bindingsHandlers;

    public void BeginProcess()
    {
        ThreadPool.QueueUserWorkItem(o => Process());
    }

    protected void Process()
    {
        string pageId = _context.Request.Params["pageId"];
        string lastChangesResult = _context.Request.Params["lastChangesResult"]; // The change results from the previous request.

        JavaScriptSerializer jss = new JavaScriptSerializer();
        ServerBindingValuesRespose[] lastResponses = jss.Deserialize<ServerBindingValuesRespose[]>(lastChangesResult);

        foreach (var sbvr in lastResponses)
        {
            BindingsHandler bh = BinderContext.Instance.GetBindingsHandler(pageId, sbvr.BindingId);
            if (bh != null)
            {
                bh.UpdateClientCounters(sbvr.PropertiesValues);
            }
        }

        _bindingsHandlers = BinderContext.Instance.GetBindingsHandlers(pageId);

        if (_bindingsHandlers.Any(b => b.HasUpdatedValue))
        {
            Complete();
        }
        else
        {
            _bindingsHandlers.ForEach(b => b.PropertyValueChanged += OnPropertyValueChanged);
        }
    }

    protected void OnPropertyValueChanged(object source, PropertyValueChangedEventArgs e)
    {
        AsyncComplete();
    }

    private void AsyncComplete()
    {
        _bindingsHandlers.ForEach(b => b.PropertyValueChanged -= OnPropertyValueChanged);

        Complete();
    }

    protected void Complete()
    {
        BinderContext.Instance.BeginInvoke(
            () =>
            {
                List<ServerBindingValuesRespose> resList = _bindingsHandlers.Where(b => b.HasUpdatedValue).
                    Select(b => new ServerBindingValuesRespose
                    {
                        BindingId = b.BindingId,
                        PropertiesValues = b.GetUpdatedValuesResponses()
                    }).ToList();

                ThreadPool.QueueUserWorkItem(
                    o =>
                    {
                        JavaScriptSerializer jss = new JavaScriptSerializer();
                        string res = jss.Serialize(o);
                        _context.Response.Write(res);

                        _isCompleted = true;
                        _callback(this);
                    }, resList);
            });
    }
}

internal class ServerBindingValuesRespose
{
    public string BindingId { get; set; }

    public List<ServerPropertyValueRespose> PropertiesValues { get; set; }
}

In the Process method, we update the change-counters with the client's properties state and, complete the request. If there are properties' changes, we call the Complete method immediately. Otherwise, we call it when there is a property change.

In the Complete method, we get the whole of the properties' changes and, create a response using them.

For routing the changes requests to our asynchronous HTTP handler, we can change our HTTP router as follows:

internal class BinderRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        string requestId = requestContext.HttpContext.Request.Params["requestId"];

        if (requestId == "propertyChangeRequest")
        {
            return new BinderHttpAsyncHandler();
        }

        return new BinderHttpHandler();
    }
}
Living with browsers connections limitation

So, we have an asynchronous mechanism for the server side but, what about the client? As some of you probably noticed, we have one AJAX request, for the whole of the properties' changes of the page. If we'll perform an AJAX request for each property, since the browser holds an opened socket until the request is completed, we'll easily exceed the limit of the opened connections of the browser. So, the problem is solved for a single page. But, if we'll open many tabs (with pages that hold connections), we'll see that some of the tabs cannot be loaded (cannot create a connection for processing the request). For solving that issue, we return a response (and free the browser connection), also after an elapsed timeout:

internal class ChangeRequestAsyncResult : IAsyncResult
{
    // ...

    private System.Timers.Timer _waitIntervalTimer;

    protected void Process()
    {
        // ...

        _bindingsHandlers = BinderContext.Instance.GetBindingsHandlers(pageId);

        if (_bindingsHandlers.Any(b => b.HasUpdatedValue))
        {
            Complete();
        }
        else
        {
            uint waitInterval = BinderContext.Instance.ChangeRequestWaitInterval;
            if (waitInterval > 0)
            {
                _waitIntervalTimer = new System.Timers.Timer();
                _waitIntervalTimer.AutoReset = false;
                _waitIntervalTimer.Interval = waitInterval;
                _waitIntervalTimer.Elapsed += OnWaitIntervalTimerElapsed;
                _waitIntervalTimer.Start();
            }

            _bindingsHandlers.ForEach(b => b.PropertyValueChanged += OnPropertyValueChanged);
        }
    }

    void OnWaitIntervalTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        BinderContext.Instance.Invoke(() =>
        {
            if (_waitIntervalTimer != null)
            {
                AsyncComplete();
            }
        });
    }

    private void AsyncComplete()
    {
        if (_waitIntervalTimer != null)
        {
            _waitIntervalTimer.Elapsed -= OnWaitIntervalTimerElapsed;
            _waitIntervalTimer.Close();
            _waitIntervalTimer = null;
        }

        _bindingsHandlers.ForEach(b => b.PropertyValueChanged -= OnPropertyValueChanged);

        Complete();
    }

    // ...
}

Notify about client collections changes

Send notification on collections changes

In order to synchronize the server with the client's collections' state, in addition to sending notifications about collection's elements properties changes, we have to send notifications also about added and removed elements. At first thought, it seemed like it could be sufficient to send notifications only about the specific changes (the indices of the removed elements, the indices of the added elements and, the new elements' properties' values). This approach can work fine, if we have only one client. But, if we have some clients, we can get unexpected results.

Suppose we have a collection with 3 elements and, 2 clients that view a page with that collection. Now, each client removes the second element of the collection (simultaneously). Each client expects to see a collection with 2 elements (the first and the third). But, let's see what really happens. When the clients remove the element, they send a notifications with the element's index. The server receives the first notification and, removes the second element of the collection. Then, the server receives the second notification and, again, removes the second element of the collection (which was originally the third element). Then, the server updates the clients with the new collection (which has only one element...). As a result, both of the clients show only one element.

For preventing such kind of things, instead of sending notification only about the specific changes, we send a full snapshot of the new collection:

/*--- Holds the properties' paths of the arrays that have been changed, for each binding id. ---*/
var _pendingChangedArraysPathes_ = {};

function _sendArraysChangeNotification_() {

    var arraysNotification = "[";

    var hasNotifications = false;

    var isFirstBindingId = true;
    for (var _bindingId_ in _pendingChangedArraysPathes_) {
        var pendingArrays = _pendingChangedArraysPathes_[_bindingId_];

        if (pendingArrays && pendingArrays.length > 0) {
            if (isFirstBindingId) {
                isFirstBindingId = false;
            } else {
                arraysNotification += ",";
            }

            hasNotifications = true;

            arraysNotification += '{"BindingId":"' + _bindingId_ + '","CollectionsNotifications":[';

            var isFirstArrayPath = true;
            while (pendingArrays.length > 0) {

                var currArrayPath = pendingArrays.shift();
                var arrHolder = _getPropertyObject_(_bindingId_, currArrayPath);
                var arrVal = arrHolder ? self.getArrayValue(arrHolder) : null;

                if (arrVal) {
                    if (isFirstArrayPath) {
                        isFirstArrayPath = false;
                    } else {
                        arraysNotification += ",";
                    }

                    var currArrCount = arrVal.length;
                    arraysNotification += '{"PropertyPath":"' + currArrayPath + '","NewCount":' + currArrCount +
                        ',"PropertiesNotifications":[';

                    var isFirstSubProp = true;
                    for (var elemInx = 0; elemInx < currArrCount; elemInx++) {
                        var currElemPath = currArrayPath + "[" + elemInx + "]";
                        var subPropPathes = _getSubPropertiesPathes_(_bindingId_, currElemPath);

                        if (subPropPathes.length > 0) {
                            for (var subPropInx = 0; subPropInx < subPropPathes.length; subPropInx++) {
                                var currSubPropPath = currElemPath + '.' + subPropPathes[subPropInx];
                                if (_isArray_(_bindingId_, currSubPropPath)) {
                                    /*--- This sub-property is an array - 
                                    Push it for array change notification. ---*/
                                    _addPendingChangedArrayPath_(_bindingId_, currSubPropPath);
                                } else {
                                    if (isFirstSubProp) {
                                        isFirstSubProp = false;
                                    } else {
                                        arraysNotification += ",";
                                    }

                                    arraysNotification += _createPropertyPathNotification_(_bindingId_, currSubPropPath);
                                }
                            }
                        } else {
                            /*--- There are no sub property-pathes... ---*/

                            if (_isArray_(_bindingId_, currElemPath)) {
                                /*--- This sub-property is an array - 
                                Push it for array change notification. ---*/
                                _addPendingChangedArrayPath_(_bindingId_, currElemPath);
                            } else {
                                if (isFirstSubProp) {
                                    isFirstSubProp = false;
                                } else {
                                    arraysNotification += ",";
                                }

                                arraysNotification += _createPropertyPathNotification_(_bindingId_, currElemPath);
                            }
                        }
                    }

                    arraysNotification += "]}";
                }
            }

            arraysNotification += ']}';
        }
    }

    arraysNotification += "]";

    if (hasNotifications) {
        var postData = "requestId=arraysChangedNotification" +
            "&pageId=" + self.pageId + "&arraysNotification=" + arraysNotification;

        _sendAjaxPost_(postData, function (xmlhttp) { });
    }
}

function _getSubPropertiesPathes_(_bindingId_, ownerPropPath) {
    var _rootObjId_ = _getBindingObjectId_(_bindingId_);
    var _propId_ = _removeArrayIndicesNumbers_(ownerPropPath);
        
    var propPathes = [];
    var objPropPathes = _getObjectPropertiesNames_(_rootObjId_, _propId_);
    if (objPropPathes) {
        for (var objPropPathInx = 0; objPropPathInx < objPropPathes.length; objPropPathInx++) {
            propPathes.push(objPropPathes[objPropPathInx]);
        }
    }

    var propInx = 0;

    while (propInx < propPathes.length) {
        var currPropPath = propPathes[propInx];
        var subPropPathes = _getObjectPropertiesNames_(_rootObjId_, _propId_ + '.' + currPropPath);
        if (subPropPathes && subPropPathes.length > 0) {
            /*--- This property has sub-properties - 
                    Replace it with its sub-properties' paths. ---*/
                
            propPathes.splice(propInx, 1);
                
            for (var subPropInx = 0; subPropInx < subPropPathes.length; subPropInx++) {
                propPathes.push(currPropPath + '.' + subPropPathes[subPropInx]);
            }
        } else {
            /*--- This property is a simple type - 
                    Go to the next property. ---*/
            propInx++;
        }
    }

    return propPathes;
}

function _isArray_(_bindingId_, _propPath_) {
    var _rootObjId_ = _getBindingObjectId_(_bindingId_);
    var _propId_ = _removeArrayIndicesNumbers_(_propPath_) + '[]';
    var objPropNames = _getObjectPropertiesNames_(_rootObjId_, _propId_);

    return objPropNames ? true : false;
}

In the _getSubPropertiesPathes_ function, we get the properties' paths for the collections element.

In the _sendArraysChangeNotification_ function, we build a notification message that contains snapshots of the changed collections and, send an AJAX request with that message.

Apply new collection's elements count

For updating a collection's count, we can add or remove elements to that collection. That can be done as follows:

public uint CollectionElementsCount { get; private set; }

public void SetCurrentCollectionElementsCount(uint newCount)
{
    IList currCollection = GetCurrentValue() as IList;
    if (null == currCollection)
    {
        return;
    }

    uint currCount = CollectionElementsCount;

    // Remove old elements if needed.
    if (currCount > newCount)
    {
        uint countDiff = currCount - newCount;

        for (uint removedInx = 0; removedInx < countDiff; removedInx++)
        {
            currCollection.RemoveAt((int)newCount);
        }
    }

    // Add new elements if needed.
    if (newCount > currCount)
    {
        Type elementType = GetElementType(currCollection.GetType());
        if (elementType != null)
        {
            uint countDiff = newCount - currCount;
            for (uint addedInx = 0; addedInx < countDiff; addedInx++)
            {
                object addedItem = CreateObject(elementType);
                currCollection.Add(addedItem);
            }
        }
    }
}

private object CreateObject(Type t)
{
        object res = null;
 
        try
        {
            res = Activator.CreateInstance(t);
        }
        catch (Exception ex)
        {
            // There was a problem to construct the object using a default constructor.
            // So, try the other constructors.
 
            ConstructorInfo[] typeConstructors = t.GetConstructors();
 
            foreach (ConstructorInfo ctor in typeConstructors)
            {
                object[] ctorParametersValues = new object[ctor.GetParameters().Count()];
 
                try
                {
                    res = Activator.CreateInstance(t, ctorParametersValues);
                }
                catch (Exception ex2)
                {
                }
 
                if (res != null)
                    break;
            }
        }

    return res;
}

In addition of adding or removing elements, we have to add or remove appropriate PropertyBindingHandler objects for handling the bindings of the appropriate collection's elements. That can be done as follows:

public void SetCurrentCollectionElementsCount(uint newCount)
{
    // ...

    ApplyNewCollectionCount(newCount);
}

private void ApplyNewCollectionCount(uint newCount)
{
    if (CollectionElementsCount != newCount)
    {
        UpdateCollectionElementsBindings(CollectionElementsCount, newCount);

        CollectionElementsCount = newCount;

        ChangeCounter++;
    }
}

private void UpdateCollectionElementsBindings(uint oldCount, uint newCount)
{
    if (oldCount < newCount)
    {
        for (uint addedInx = oldCount; addedInx < newCount; addedInx++)
        {
            AddCollectionElementBindings(addedInx);
        }
    }
    else if (oldCount > newCount)
    {
        for (uint removedInx = newCount; removedInx < oldCount; removedInx++)
        {
            RemoveCollectionElementBindings(removedInx);
        }
    }
}

private void AddCollectionElementBindings(uint elementInx)
{
    string elemClientPropertyPath = string.Format("{0}[{1}]", ClientPropertyPath, elementInx);
    string elemServerPropertyPath = string.Format("{0}[{1}]", ServerPropertyPath, elementInx);

    if (_mapping.HasCollectionElementMapping)
    {
        foreach (BindingMapping elemPropMapping in _mapping.CollectionElementMapping)
        {
            _rootBindingsHandler.AddPropertiesHandlers(_rootObject, elemPropMapping,
                elemServerPropertyPath, elemClientPropertyPath);
        }
    }
    else
    {
        _rootBindingsHandler.AddPropertiesHandlers(_rootObject,
            BindingMapping.FromPropertyMapping(string.Empty, string.Empty, _mapping.RootMapping.MappingMode),
            elemServerPropertyPath, elemClientPropertyPath);
    }
}

private void RemoveCollectionElementBindings(uint elementInx)
{
    string elemClientPropertyPath = string.Format("{0}[{1}]", ClientPropertyPath, elementInx);
    _rootBindingsHandler.RemovePropertyBindingHandlersTree(elemClientPropertyPath);            
}
Process collections changes request

For getting the request's content, we create an equivalent data-structure (the same structure as the JSON string of the arraysNotification request's field):

public class CollectionsValuesNotificationData
{
    public string BindingId { get; set; }
    public List<CollectionValuesData> CollectionsNotifications { get; set; }
}

public class CollectionValuesData
{
    public string PropertyPath { get; set; }
    public uint NewCount { get; set; }
    public List<PropertyValueData> PropertiesNotifications { get; set; }
}

Using that data-structure, we can handle the collections-notification request to apply the new collections' values, in the same manner as we handle the properties changes request:

internal class BinderHttpHandler : IHttpHandler
{        
    // ...

    public void ProcessRequest(HttpContext context)
    {
        string requestId = context.Request.Params["requestId"];

        switch (requestId)
        {
            // ...

            case "arraysChangedNotification":
                {
                    HandleClientArraysChangedNotificationRequest(context);
                    break;
                }
        }
    }

    // ...

    protected bool HandleClientArraysChangedNotificationRequest(HttpContext context)
    {
        string pageId = context.Request.Params["pageId"];
        string arraysNotificationStr = context.Request.Params["arraysNotification"];

        JavaScriptSerializer jss = new JavaScriptSerializer();
        CollectionsValuesNotificationData[] arraysNotification =
            jss.Deserialize<CollectionsValuesNotificationData[]>(arraysNotificationStr);

        BinderContext.Instance.PostCollectionsUpdate(pageId, arraysNotification);

        return true;
    }
}

public class BinderContext : IDisposable
{
    // ...

    public void UpdateCollections(string pageId, CollectionsValuesNotificationData[] collectionsNotification)
    {
        foreach (CollectionsValuesNotificationData cnd in collectionsNotification)
        {
            BindingsHandler bh = GetBindingsHandler(pageId, cnd.BindingId);
            if (bh != null)
            {
                foreach (var cn in cnd.CollectionsNotifications)
                {
                    bh.SetCurrentCollectionElementsCount(cn.PropertyPath, cn.NewCount);

                    foreach (var pn in cn.PropertiesNotifications)
                    {
                        bh.SetCurrentValue(pn.PropertyPath, pn.NewValue);
                    }
                }
            }
        }
    }

    public void PostCollectionsUpdate(string pageId, CollectionsValuesNotificationData[] collectionsNotification)
    {
        BeginInvoke(() => UpdateCollections(pageId, collectionsNotification));
    }

    // ...
}

Notify about server collections changes

In order to create a notification about the changed collections, we have to be notified about the collections' changes. If the full collection has been replaced (another reference is set), we can get the notification in the same way we get the notification for other properties (by implementing the INotifyPropertyChanged interface in the source object). But, if the collection itself is changed (elements added or removed), we need another way to get the notification. For that purpose, we have the INotifyCollectionChanged interface. In the case of properties changes, we get the notification automatically by the binding (WPF binding registers to the INotifyPropertyChanged.PropertyChanged event). In the case of collections changes, we have to implement it by ourselves:

public class PropertyBindingHandler : DependencyObject, IDisposable
{
    // ...

    private static void OnCurrentValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        // ...

        if (pbh.IsCollection)
        {
            INotifyCollectionChanged oldCollection = e.OldValue as INotifyCollectionChanged;
            if (oldCollection != null)
            {
                oldCollection.CollectionChanged -= pbh.OnCollectionChanged;
            }

            INotifyCollectionChanged newCollection = e.NewValue as INotifyCollectionChanged;
            if (newCollection != null)
            {
                newCollection.CollectionChanged += pbh.OnCollectionChanged;
            }

            uint elementsCount = pbh.GetEnumerableCount(e.NewValue as IEnumerable);
            pbh.ApplyNewCollectionCount(elementsCount);
        }

        // ...
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        uint newCollectionCount = GetEnumerableCount(sender as IEnumerable);
        uint oldCollectionCount = CollectionElementsCount;

        ApplyNewCollectionCount(newCollectionCount);

        // Raise an event about the change.
        EventHandler<ValueChangedEventArgs> handler = CurrentValueChanged;
        if (null != handler)
        {
            ValueChangedEventArgs args = new ValueChangedEventArgs
                                             {
                                                 OldValue = oldCollectionCount,
                                                 NewValue = newCollectionCount
                                             };

            // Run the event handler on the dispatcher's thread,
            // in order to synchronize it with the change-request asynchronous result.
            if (CheckAccess())
            {
                handler(this, args);
            }
            else
            {
                Dispatcher.BeginInvoke(new Action(() => handler(this, args)));
            }
        }
    }

    // ...
}

When the value of the CurrentValue property is changed, we register to the INotifyCollectionChanged.CollectionChanged event and, apply the new collection's count.

When the collection is changed, we apply the new collection's count and, raise an event about the property's change.

When getting the updated value response, we set the collection's count instead of current property value (the collection itself):

public bool IsCollection { get { return _mapping.RootMapping.IsCollection; } }

public ServerPropertyValueRespose GetUpdatedValueResponse()
{
    // If this is a collection, the NewValue is the new collection's elements count,
    // otherwise, the NewValue is the current binded value.
    return new ServerPropertyValueRespose
    {
        SubPropertyPath = ClientPropertyPath,
        ChangeCounter = ChangeCounter,
        NewValue = IsCollection ? CollectionElementsCount.ToString() : GetCurrentValueString(),
        IsCollection = IsCollection
    };
}

In order to apply the server changes on the client side, we have to change the client-side collection according to the received notification. That can be done as follows:

function _applyServerChangesResponse_(response) {
    // ...

    for (var propertyResultInx = 0; propertyResultInx < currProperties.length; propertyResultInx++) {
        var currPropertyResult = currProperties[propertyResultInx];

        if (currPropertyResult.IsCollection) {
            _applyServerCollectionChange_(currElement.BindingId, 
                currPropertyResult.SubPropertyPath, currPropertyResult.NewValue);
        } else {
            _applyServerPropertyChange_(currElement.BindingId, 
                currPropertyResult.SubPropertyPath, currPropertyResult.NewValue);
        }
    }

    // ...
}

function _applyServerCollectionChange_(_bindingId_, _propPath_, newCount) {
    var propObject = _getPropertyObject_(_bindingId_, _propPath_);
    if (propObject) {
        var arr = self.getArrayValue(propObject);
        if (!arr) {
            arr = [];
        }

        var orgCount = arr.length;
        var lengthDiffernce = newCount - orgCount;

        if (lengthDiffernce > 0) {
            for (var elemInx = 0; elemInx < lengthDiffernce; elemInx++) {
                arr.push(_createBoundObjectTree_(_bindingId_, _propPath_ + "[" + (orgCount + elemInx) + "]"));
            }
            self.setArrayValue(propObject, arr);
        }

        if (lengthDiffernce < 0) {
            arr.splice(newCount, Math.abs(lengthDiffernce));
            self.setArrayValue(propObject, arr);
        }
    }
}

function _createBoundObjectTree_(_bindingId_, _propPath_) {
    /*--- Create the object ---*/
    var _rootObjId_ = _getBindingObjectId_(_bindingId_);
    var _propId_ = _removeArrayIndicesNumbers_(_propPath_);
    var objHolder = _createObject_(_rootObjId_, _propId_);

    /*--- Subscribe for notifications ---*/
    _registerForChanges_(_bindingId_, objHolder, _propPath_);

    return objHolder;
}

In the _applyServerCollectionChange_ function, we add or remove elements according to the new collection's count.

In the _createBoundObjectTree_ function, we create a new object for the collection's element and, register for changes notifications for that object.

Register for client changes notifications

In order to send notifications about client objects changes, we have to register to that changes. That can be done as follows:

this.applyBindings = function () {
    /*--- Create the objects, if they aren't created yet and, 
            subscribe for notifications ---*/
    /*--- Because some bindings can share a same root object,
            some of the notifications can be sent also for unrelated bindings. -
            Those notifications are ignored in the server. ---*/
    for (var _bindingId_ in _bindingsObjectsIds_) {
        var _rootObjId_ = _getBindingObjectId_(_bindingId_);
        var objCreators = _getBindingObjectsCreators_(_rootObjId_);
        if (objCreators) {
            for (var _propId_ in objCreators) {
                if (_propId_ != "" &&
                    _propId_.indexOf(".") < 0 &&
                        _propId_.indexOf("[") < 0) {
                    /*--- This is a root property.
                            (The property-id is equal to the property-name,
                            for a root property) ---*/

                    _bindRootPropertyTree_(_bindingId_, _propId_);
                }
            }
        }
    }
};

function _bindRootPropertyTree_(_bindingId_, propName) {
    var rootObj = _getBindingObject_(_bindingId_);

    if (rootObj) {
        var objHolder = rootObj[propName];

        if (!objHolder) {
            var _rootObjId_ = _getBindingObjectId_(_bindingId_);
            var _propId_ = _removeArrayIndicesNumbers_(propName);
            objHolder = _createObject_(_rootObjId_, _propId_);
            rootObj[propName] = objHolder;
        }

        /*--- Subscribe for notifications ---*/
        _registerForChanges_(_bindingId_, objHolder, propName);
    }
}

function _registerForChanges_(_bindingId_, objHolder, _propPath_) {
    var _rootObjId_ = _getBindingObjectId_(_bindingId_);
    var _propId_ = _removeArrayIndicesNumbers_(_propPath_);
    var arrayElemId = _propId_ + "[]";
    var arrayElemPropNames = _getObjectPropertiesNames_(_rootObjId_, arrayElemId);

    if (arrayElemPropNames) {
        /*--- If there is an array for the elements properties' names,
                this is an array. ---*/

        /*--- Register to the array's changes ---*/
        var arrayNotificationFunc = function () {
            _addPendingChangedArrayPath_(_bindingId_, _propPath_);
            setTimeout(function () {
                _sendArraysChangeNotification_();
            }, 0);
        };
            
        self.registerForArrayChanges(objHolder, arrayNotificationFunc);

        var arrVal = self.getArrayValue(objHolder);
        if (arrVal) {
            for (var elemInx = 0; elemInx < arrVal.length; elemInx++) {
                _registerForChanges_(_bindingId_, arrVal[elemInx], _propPath_ + '[' + elemInx + ']');
            }
        }
            
    } else {
        var objVal = self.getObjectValue(objHolder);
        if (objVal) {
            var subPropNames = _getObjectPropertiesNames_(_rootObjId_, _propId_);
            if (subPropNames && subPropNames.length > 0) {
                for (var subPropInx = 0; subPropInx < subPropNames.length; subPropInx++) {
                    var currSubPropPath = _propPath_;
                    var currSubPropName = subPropNames[subPropInx];
                    if (currSubPropPath != "" && currSubPropName != "") {
                        currSubPropPath += ".";
                    }
                    currSubPropPath += currSubPropName;
                    var propObjHolder = objVal[subPropNames[subPropInx]];

                    _registerForChanges_(_bindingId_, propObjHolder, currSubPropPath);
                }
            } else {
                /*--- Register to the property's changes ---*/
                var propNotificationFunc = function() {
                    _addPendingChangedPropertyPath_(_bindingId_, _propPath_);
                    setTimeout(function() {
                        _sendPropertiesChangeNotification_();
                    }, 0);
                };
                    
                self.registerForPropertyChanges(objHolder, propNotificationFunc);
            }
        }
    }
}

In the applyBindings function, we go over the whole of the root properties (properties of the binding's root object) and, call the _bindRootPropertyTree_ function with them.

In the _bindRootPropertyTree_ function, we get the object holder for a property of the binding's root object and, register for changes notifications for that object.

In the _registerForChanges_ function, we register for changes notifications for the given property-path and its sub-property-pathes. In order to register for arrays and simple properties changes, we create a function (for notifying about the change) and, register it using the registerForArrayChanges and registerForPropertyChanges functions appropriately. Since we have a generic implementation, those functions can be implemented differently for different JavaScript libraries.

When a property's value is changed by a server update, we don't have to send this value back to the server. Since we queue the sending of the notifications for a later execution (using the setTimeout function), we can cancel unneeded notifications. For canceling a property's notification, we can remove its property-path from the pending changes array:

function _applyServerPropertyChange_(_bindingId_, _propPath_, newValue) {
    var propObject = _getPropertyObject_(_bindingId_, _propPath_);
    if (propObject) {
        // ...

        /*--- The value of the property has been just updated from the server.
                We don't need to send this value back... ---*/
        _removePendingChangedPropertyPath_(_bindingId_, _propPath_);
    }
}

function _applyServerCollectionChange_(_bindingId_, _propPath_, newCount) {
    var propObject = _getPropertyObject_(_bindingId_, _propPath_);
    if (propObject) {
        // ...

        /*--- The value of the array has been just updated from the server.
                We don't need to send this value back... ---*/
        _removePendingChangedArrayPath_(_bindingId_, _propPath_);
    }
}

Apply Page Bindings

Apply server bindings

Since there can be some binding-mappings for a page, we need a way to identify each binding-mapping of the page. For that purpose, we add a class that holds the whole of the binding-mappings of the page and, registers each one with a different identifier:

public abstract class BinderDefinitions
{
    private uint _bindingIdCounter;

    public string PageId { get; set; }

    #region Bindings

    private List<BindingData> _bindings;

    protected List<BindingData> Bindings
    {
        get { return _bindings ?? (_bindings = new List<BindingData>()); }
    }

    #endregion

    protected void AddBinding(string clientObjectString,
        object serverObject, BindingMapping objectBindingMapping)
    {
        _bindingIdCounter++;

        string bindingId = string.Format("B{0}", _bindingIdCounter);

        BindingData bd = new BindingData(bindingId, clientObjectString, serverObject, objectBindingMapping);

        Bindings.Add(bd);
    }

    public void ApplyServerBindings()
    {
        foreach (BindingData bd in Bindings)
        {
            BinderContext.Instance.RegisterBinding(PageId, bd.BindingId, bd.ServerObject,
                                    bd.ObjectBindingMapping, false);
        }
    }
}

public class BindingData
{
    public BindingData(string bindingId, string clientObjectString,
        object serverObject, BindingMapping objectBindingMapping)
    {
        BindingId = bindingId;
        ClientObjectString = clientObjectString;
        ServerObject = serverObject;
        ObjectBindingMapping = objectBindingMapping;
    }

    #region Properties

    public string BindingId { get; set; }
    public string ClientObjectString { get; set; }
    public object ServerObject { get; set; }
    public BindingMapping ObjectBindingMapping { get; set; }

    #endregion
}

Inject client script

Generate the script

So, we have a class that holds the whole of the page's binding-mappings. The next step is to generate an appropriate client script.

The first script we have to provide is, the client's object model implementation. One approach for doing that is, providing a JavaScript file that contains the script. In that way, every user of our library can add a script tag with a reference to our script file. But, inserting client script separately, can lead to some problems: the user can forget to include the script file or, even if the user has included the script file, the script version can be incompatible with the server-side version.

In order to ensure that the client-side script is compatible with the server-side code, we take another approach and, generate the client-side script for each page.

For generating the client's object model script, we add the script file as a resource of our project and, use it for returning the script's string:

protected static object ResourcesLocker { get; private set; }

private const string _originalBinderClientConstructorFunctionName = "_WebBindingBinderClient_";

public string BinderClientConstructorFunctionName { get; set; }

private string GetBinderClientScript()
{
    string res = string.Empty;

    Uri resUri = new Uri("/WebBinding;component/Scripts/BinderClient.js", UriKind.Relative);

    lock (ResourcesLocker)
    {
        StreamResourceInfo resInfo = Application.GetResourceStream(resUri);
        if (resInfo != null)
        {
            using (StreamReader sr = new StreamReader(resInfo.Stream))
            {
                res = sr.ReadToEnd();
            }
        }
    }

    res = Regex.Replace(res, _originalBinderClientConstructorFunctionName, BinderClientConstructorFunctionName);

    return res;
}

Since the client's object model script is a generic script, we have to inject the dedicate implementation too. That can be done by implementing a dedicate class for the dedicate library (in our case: the Knockout.js library):

public class KnockoutBinderDefinitions : BinderDefinitions
{
    private const string _originalApplyDedicateImplementationFunctionName = "WebBinding_ApplyKnockoutDedicateImplementation";

    public KnockoutBinderDefinitions()
    {
        ApplyDedicateImplementationFunctionName = "WebBinding_ApplyKnockoutDedicateImplementation";
    }

    #region Properties
    public string ApplyDedicateImplementationFunctionName { get; set; }
    #endregion

    #region BinderDefinitions implementation

    protected override string GetApplyDedicateImplementationScript()
    {
        StringBuilder sb = new StringBuilder();

        sb.AppendLine(GetDedicateImplementationScript());
        sb.AppendFormat("{0}({1});", ApplyDedicateImplementationFunctionName, BinderClientObjectName);

        return sb.ToString();
    }

    #endregion

    private string GetDedicateImplementationScript()
    {
        string res = string.Empty;

        Uri resUri = new Uri("/WebBinding.Knockout;component/Scripts/KnockoutDedicateImplementation.js", UriKind.Relative);

        lock (ResourcesLocker)
        {
            StreamResourceInfo resInfo = Application.GetResourceStream(resUri);
            if (resInfo != null)
            {
                using (StreamReader sr = new StreamReader(resInfo.Stream))
                {
                    res = sr.ReadToEnd();
                }
            }
        }

        res = Regex.Replace(res, _originalApplyDedicateImplementationFunctionName, ApplyDedicateImplementationFunctionName);

        return res;
    }
}

In that class, we implement a virtual method (GetApplyDedicateImplementationScript) that generates the dedicate script.

After we have the client's object model script, we can generate a client script for applying the page's binding-mappings:

public string BinderClientObjectName { get; set; }

public virtual string GetBinderScript()
{
    StringBuilder clientScript = new StringBuilder();

    // Add the client's object model script.
    clientScript.Append(GetBinderClientScript());
    clientScript.AppendLine();

    // Create an instance of the client's object model.
    clientScript.AppendFormat("var {0}=new {1}(\"{2}\");",
                                BinderClientObjectName, BinderClientConstructorFunctionName, PageId);
    clientScript.AppendLine();

    // Add dedicate implementation script.
    clientScript.Append(GetApplyDedicateImplementationScript());
    clientScript.AppendLine();

    // Start receiving server changes notifications.
    clientScript.AppendFormat("{0}.beginServerChangesRequests();", BinderClientObjectName);
    clientScript.AppendLine();

    // Construct the client model for each binding.
    foreach (BindingData bd in Bindings)
    {
        clientScript.Append(GetBindingRegistrationScript(bd));
        clientScript.AppendLine();
    }

    // Register for client changes notifications.
    clientScript.AppendFormat("{0}.applyBindings();", BinderClientObjectName);

    return clientScript.ToString();
}

protected abstract string GetApplyDedicateImplementationScript();

private string GetBindingRegistrationScript(BindingData bd)
{            
    StringBuilder sb = new StringBuilder();

    string mappingObjectString = bd.ObjectBindingMapping.ToClientBindingMappingObjectString();

    sb.AppendFormat("{0}.addBindingMapping(\"{1}\",{2},{3});",
                    BinderClientObjectName, bd.BindingId, bd.ClientObjectString, mappingObjectString);

    return sb.ToString();
}
Additional functionality

In addition to the basic functionality, we add an option for automatically creating the root objects of the bindings, if they don't exist:

public bool DefineRootObjectsIfNotExist { get; set; }

private string GetBindingRegistrationScript(BindingData bd)
{            
    // ...

    if (DefineRootObjectsIfNotExist)
    {
        sb.Append("((function() {if (!this[\"").Append(bd.ClientObjectString).Append("\"]){this[\"").
            Append(bd.ClientObjectString).Append("\"]={};}})());");
    }

    // ...
}

It is more recommended to define those objects manually but, that can be a good solution for simple cases.

Sometimes, we may want to create new objects (like adding collection's elements) in the bound model. Since we deal with a bound model, we can create those objects in the server side (using Web API or something like) and, they will be reflected to the whole of the clients. But, if we still want to create the objects in the client-side (and want their changes to be reflected to the server), we have to register for their properties' changes too. For that purpose, we add the following function:

this.createBoundObjectForPropertyPath = function (rootObj, _propPath_) {
    /*--- get the object id ---*/
    var _rootObjId_ = null;
    for (var objId in _rootBindingObjects_) {
        if (_rootBindingObjects_[objId] == rootObj) {
            _rootObjId_ = objId;
        }
    }

    if (_rootObjId_) {
        var _propId_ = _removeArrayIndicesNumbers_(_propPath_);
        var objHolder = _createObject_(_rootObjId_, _propId_);

        for (var _bindingId_ in _bindingsObjectsIds_) {
            if (_bindingsObjectsIds_[_bindingId_] == _rootObjId_) {
                _registerForChanges_(_bindingId_, objHolder, _propPath_);
            }
        }

        return objHolder;
    }

    return null;
};

In that function, we create an object according to the given property-path using the stored creator function and, register for properties' changes for the whole of the bindings that use the given root object.

Additional data we may want to use in the client side is the current page identifier. We can generate functions for creating bound objects and for getting the page's identifier, as follows:

public string CreateBoundObjectFunctionName { get; set; }
public string GetPageIdFunctionName { get; set; }

private string GetAdditionalFunctionsScript()
{
    StringBuilder sb = new StringBuilder();

    if (!string.IsNullOrEmpty(CreateBoundObjectFunctionName))
    {                
        sb.Append("function ").Append(CreateBoundObjectFunctionName).
            Append("(o,p){return ").Append(BinderClientObjectName).
            Append(".createBoundObjectForPropertyPath(o,p);}");                
    }

    if (!string.IsNullOrEmpty(GetPageIdFunctionName))
    {
        sb.Append("function ").Append(GetPageIdFunctionName).
            Append("(){return ").Append(BinderClientObjectName).Append(".pageId;}");
    }

    return sb.ToString();
}
Minimize injected javascript code

In order to minimize the data amount that is transferred for the page, standard JavaScript libraries (e.g. jQuery, Knockout, etc.) are provided also by minimized JavaScript files. We take the same approach, also for our library:

private static readonly Dictionary<string, string> _defaultVariablesReplacementValues;

static BinderDefinitions()
{
    // ...

    _defaultVariablesReplacementValues =
        new Dictionary<string, string>
            {
                {"_bindingsObjectsIds_", "a1"},
                {"_rootBindingObjects_", "a2"},
                {"_objectsCreators_", "a3"},
                        
                // ...

                {"_applyServerChangesResponse_", "i7"},
                {"_applyServerPropertyChange_", "i8"},
                {"_applyServerCollectionChange_", "i9"}
            };
}

protected BinderDefinitions()
{
    // ...

    VariablesReplacementValues = new Dictionary<string, string>(_defaultVariablesReplacementValues);

    // ...
}

// Dictionary of <regex-pattern, replacement-string>, for replacements in the client script.
protected Dictionary<string, string> VariablesReplacementValues { get; private set; }

public bool MinimizeClientScript { get; set; }

private string GetBinderClientScript()
{
    // ...

    if (MinimizeClientScript)
    {
        res = GetMinimizedBinderClientScript(res);
    }

    // ...
}

private string GetMinimizedBinderClientScript(string originalScript)
{
    // Remove comments.
    string res = Regex.Replace(originalScript, "/\\*-{3}([\\r\\n]|.)*?-{3}\\*/", string.Empty);

    // Replace variables names
    foreach (var variableReplacement in VariablesReplacementValues)
    {
        res = Regex.Replace(res, variableReplacement.Key, variableReplacement.Value);
    }

    // Remove lines' spaces
    res = Regex.Replace(res, "[\\r\\n][\\r\\n \\t]*", string.Empty);

    // Remove additional spaces
    res = Regex.Replace(res, " ?([=\\+\\{\\},\\(\\)!\\?:\\>\\<\\|&\\]\\[-]) ?", "$1");

    return res;
}

In the GetMinimizedBinderClientScript method, we remove comments and unnecessary spaces and, change some of the variables' names to shorter names.

In the VariablesReplacementValues dictionary, we hold the replacement string, for each variable's name that should be replaced. So, in order to change the default replacements, we can change that dictionary.

Add HtmlHelper extension

After we have the client's script, we can inject it to the rendered page. For that purpose, we add an extension method to the HtmlHelper type:

public static class BinderHtmlHelperExtensions
{
    private static uint _pageIdCounter = 1;

    private static string GeneratePageId(HtmlHelper helper)
    {
        string idPrefix;

        try
        {
            string sessionId = helper.ViewContext != null && helper.ViewContext.HttpContext != null && helper.ViewContext.HttpContext.Session != null
                   ? helper.ViewContext.HttpContext.Session.SessionID
                   : "<null>";

            string conrollerName = helper.ViewContext != null && helper.ViewContext.RouteData.Values["Controller"] != null
                                       ? helper.ViewContext.RouteData.Values["Controller"].ToString()
                                       : string.Empty;


            string actionName = helper.ViewContext != null && helper.ViewContext.RouteData.Values["Action"] != null
                                       ? helper.ViewContext.RouteData.Values["Action"].ToString()
                                       : string.Empty;

            idPrefix = string.Format("{0}/{1}@{2}", conrollerName, actionName, sessionId);
        }
        catch (Exception)
        {
            idPrefix = Guid.NewGuid().ToString();
        }

        string id = string.Format("{0}#{1}", idPrefix, _pageIdCounter);

        _pageIdCounter++;

        return id;
    }

    public static IHtmlString WebBinder(this HtmlHelper helper, BinderDefinitions bd)
    {
        bd.PageId = GeneratePageId(helper);

        bd.ApplyServerBindings();

        string binderScript = bd.GetBinderScript();

        return new HtmlString(string.Format("<script type=\"text/javascript\">{0}</script>", binderScript));
    }
}

In the GeneratePageId method, we generate a unique identifier for a page, based on the ViewContext and a counting number.

In the WebBinder method, we register the server's bindings and, generates the client's script. This method returns an HtmlString object with the client's script wrapped with a script tag.

Generate Binding-mapping using Attributes

In order to simplify the creation of the binding-mapping, we add an attribute for marking the bound properties:

[AttributeUsage(AttributeTargets.Property)]
public class WebBoundAttribute : Attribute
{
    public WebBoundAttribute()
    {
        IndicateCollectionTypeAutomatically = true;
        MappingMode = BindingMappingMode.TwoWay;
        IndicateMappingModeAutomatically = true;
        RecursionLevel = 5;
    }

    public string ClientPropertyName { get; set; }
    public bool IsCollection { get; set; }
    public bool IndicateCollectionTypeAutomatically { get; set; }
    public BindingMappingMode MappingMode { get; set; }
    public bool IndicateMappingModeAutomatically { get; set; }
    public uint RecursionLevel { get; set; }
}

Using that attribute, we can generate a binding mapping for a given type:

public static BindingMapping FromType(Type serverObjectType, bool discardInvalidMappings = false)
{
    if (null == serverObjectType)
    {
        throw new ArgumentNullException("serverObjectType");
    }

    BindingMapping bm = new BindingMapping();


    Dictionary<PropertyInfo, uint> propertiesRecursionLevel = new Dictionary<PropertyInfo, uint>();

    try
    {
        AddSubPropertiesMapping(serverObjectType, bm.SubPropertiesMapping, propertiesRecursionLevel, discardInvalidMappings);
    }
    catch (InvalidOperationException ioe)
    {
        string msg = string.Format("Binding cannot fully work for type '{0}'. See inner exception for more details.",
            serverObjectType.FullName);

        throw new InvalidOperationException(msg, ioe);
    }            

    return bm;
}

private static bool IsPropertyWebBound(PropertyInfo pi, Dictionary<PropertyInfo, uint> propertiesRecursionLevel)
{
    if (!propertiesRecursionLevel.ContainsKey(pi))
    {
        WebBoundAttribute wba =
            pi.GetCustomAttributes(typeof(WebBoundAttribute), true).FirstOrDefault() as WebBoundAttribute;

        propertiesRecursionLevel[pi] = wba != null ? wba.RecursionLevel : 0;
    }

    if (propertiesRecursionLevel[pi] > 0)
    {
        propertiesRecursionLevel[pi]--;

        return true;
    }

    return false;
}

private static void AddSubPropertiesMapping(Type t, List<BindingMapping> dstMappingList,
    Dictionary<PropertyInfo, uint> basePropertiesRecursionLevel, bool discardInvalidMappings)
{
    if (t == null || dstMappingList == null || basePropertiesRecursionLevel == null)
    {
        return;
    }

    Dictionary<PropertyInfo, uint> propertiesRecursionLevel = 
        new Dictionary<PropertyInfo, uint>(basePropertiesRecursionLevel);

    IEnumerable<PropertyInfo> webBindedProperties =
        t.GetProperties().Where(p => IsPropertyWebBound(p, propertiesRecursionLevel));
    foreach (PropertyInfo pi in webBindedProperties)
    {
        BindingMapping bm;

        try
        {
            bm = new BindingMapping(PropertyMapping.FromProperty(pi));
        }
        catch (InvalidOperationException)
        {
            if (discardInvalidMappings)
            {
                continue;
            }

            throw;
        }
                
        if (bm.RootMapping.IsCollection)
        {
            Type elemType = GetElementType(pi.PropertyType);
            AddCollectionElementMapping(elemType, bm.CollectionElementMapping, propertiesRecursionLevel, discardInvalidMappings);
        }
        else
        {
            AddSubPropertiesMapping(pi.PropertyType, bm.SubPropertiesMapping, propertiesRecursionLevel, discardInvalidMappings);
        }

        dstMappingList.Add(bm);
    }

}

private static void AddCollectionElementMapping(Type elemType, List<BindingMapping> dstMappingList,
    Dictionary<PropertyInfo, uint> propertiesRecursionLevel, bool discardInvalidMappings)
{
    if (IsCollection(elemType))
    {
        BindingMapping bm = new BindingMapping();
        bm.RootMapping.IsCollection = true;

        Type subElemType = GetElementType(elemType);
        AddCollectionElementMapping(subElemType, bm.CollectionElementMapping, propertiesRecursionLevel, discardInvalidMappings);

        dstMappingList.Add(bm);
    }
    else
    {
        AddSubPropertiesMapping(elemType, dstMappingList, propertiesRecursionLevel, discardInvalidMappings);
    }
}

private static bool IsCollection(Type t)
{
    if (t == null)
    {
        return false;
    }

    if (t == typeof(string))
    {
        return false;
    }

    Type[] typeIntefaces = t.GetInterfaces();
    if (typeIntefaces.Contains(typeof(IEnumerable)))
    {
        return true;
    }

    return false;
}

private static Type GetElementType(Type containerType)
{
    Type res = null;

    if (containerType.HasElementType)
    {
        res = containerType.GetElementType();
    }

    if (containerType.IsGenericType)
    {
        Type[] genericArguments = containerType.GetGenericArguments();
        if (genericArguments.Length == 1)
        {
            res = genericArguments[0];
        }
    }

    return res;
}

In the FromType method, we create a new BindingMapping object and fill its SubPropertiesMapping according to the bound properties (properties that are marked with the WebBound attribute).

In the AddSubPropertiesMapping method, we go over the whole of the bound properties and, create an appropriate BindingMapping object for them. If the property is a collection, we fill the CollectionElementMapping according to the bound properties of the collection's element type. Otherwise, we fill the SubPropertiesMapping according to the bound properties of the property's type.

If a property's type has a descendant (a nested property) of its own type, since we generate the binding-mapping according to the type's information, we can easily get into an infinite recursion (that is usually stop in the middle by a StackOverflowException exception). In order to prevent that, we have the RecursionLevel property for each WebBound attribute. Using that property, we can ignore properties that are too deep in the recursion level. That is done by the IsPropertyWebBound method.

For creating the property-mapping for each property, we use the PropertyMapping.FromProperty method. In this method, we create a PropertyMapping object according to the property's WebBound attribute and, throw an appropriate exception when there is an invalid mapping.

Garbage Collection

In order to be notified about pages that are not in use any more, we store the last action time for each page:

// Dictionary of (page-id, last-action-time).
private readonly Dictionary<string, DateTime> _pagesLastActionTime;

public BindingsHandler GetBindingsHandler(string pageId, string bindingId)
{
    // ...

    _pagesLastActionTime[pageId] = DateTime.Now;

    // ...
}

public List<BindingsHandler> GetBindingsHandlers(string pageId)
{
    // ...

    _pagesLastActionTime[pageId] = DateTime.Now;

    // ...
}

Using the stored last actions times, we can determine which page isn't in use any more (closed). In order to clean the unneeded pages' data, we add a timer that runs a "garbage collection":

private System.Timers.Timer _gcTimer;

private void StartGarbageCollectionTimer()
{
    _gcTimer = new System.Timers.Timer();
    _gcTimer.AutoReset = true;
    _gcTimer.Interval = GarbageCollectionInterval;
    _gcTimer.Elapsed += OnGcTimerElapsed;
    _gcTimer.Start();
}

private void OnGcTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    CollectNotInUsePagesData();
}

private void StopGarbageCollectionTimer()
{
    if (_gcTimer != null)
    {
        _gcTimer.Stop();
        _gcTimer = null;
    }
}

private void CollectNotInUsePagesData()
{
    lock (Locker)
    {
        string[] unusedPagesIds = _pagesLastActionTime.
            Where(p => (DateTime.Now - p.Value).TotalMilliseconds > MaxInactivationMilliseconds).
            Select(p => p.Key).ToArray();

        foreach (string pageId in unusedPagesIds)
        {
            _pagesLastActionTime.Remove(pageId);

            if (_pagesBindings.ContainsKey(pageId))
            {
                Dictionary<string, BindingsHandler> currPagesData = _pagesBindings[pageId];
                foreach (var pd in currPagesData)
                {
                    BindingsHandler bh = pd.Value;
                    BeginInvoke(() => bh.Dispose());
                }

                _pagesBindings.Remove(pageId);
            }
        }
    }
}

In the CollectNotInUsePagesData method, we go over the whole of the pages that aren't in use and, clean their data.

In order to enable additional cleaning when a page is removed, we add an event for indicating about a removed page:

public class PageEventArgs : EventArgs
{
    public string PageId { get; set; }
}

public class BinderContext : IDisposable
{
    // ...

    public event EventHandler<PageEventArgs> PageRemoved;

    private void CollectlNotInUsePagesData()
    {
        // ...

        // Raise PageRemoved event.
        EventHandler<PageEventArgs> handler = PageRemoved;
        if (handler != null)
        {
            PageEventArgs arg = new PageEventArgs {PageId = pageId};
            handler(this, arg);
        }

        // ...
    }

    // ...
}

How To Use It

Example 1: Shared view-model vs. unique view-model

For demonstrating the use of the WebBinding library, we create an example view-model and, an ASP.NET MVC4 (can be downloaded from: http://www.asp.net/mvc/mvc4) web page that presents that view-model.

In the first example, we create 2 instances of a view-model that contains a web-bound Text property. One of the instances is shared with the whole of the pages and, the other one is unique for the specific page.

For our view-model, we create the following classes:

public class BaseViewModel : INotifyPropertyChanged
{
    #region INotifyPropertyChanged implementation
    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(string propName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propName));
        }
    }
    #endregion
}

public class ExampleViewModel : BaseViewModel
{
    #region Text

    private string _text;

    [WebBound(ClientPropertyName = "text")]
    public string Text
    {
        get { return _text; }
        set
        {
            if (_text != value)
            {
                _text = value;
                NotifyPropertyChanged("Text");
            }
        }
    }

    #endregion
}

For creating a shared instance of our view-model, we create a singleton that holds our view-model:

public class ExampleContext
{
    #region Singleton implementation

    private ExampleContext()
    {
        CommonBindPropertiesExampleViewModel = new ExampleViewModel();
    }

    private static readonly ExampleContext _instance = new ExampleContext();

    public static ExampleContext Instance
    {
        get { return _instance; }
    }

    #endregion

    #region Properties

    public ExampleViewModel CommonBindPropertiesExampleViewModel { get; private set; }

    #endregion
}

Now, let's add an action for presenting a view with a new instance of our view-model as its Model:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        ExampleViewModel vm = new ExampleViewModel();

        return View(vm);
    }
}

For applying WebBinding (binding the view-model in the server side, to a corresponding view-model in the client side) on our view, we:

  1. Add JavaScript objects for holding the corresponding view-models in the client side:
    <script type="text/javascript">
        var uniqueVm = {};
        var sharedVm = {};
    </script>
  2. Create a BinderDefinitions object that holds the binding-mappings for both of the view-models:
    @{
        BinderDefinitions bd = new KnockoutBinderDefinitions();
        bd.AddBinding("uniqueVm", ViewData.Model);
        bd.AddBinding("sharedVm", ExampleContext.Instance.CommonBindPropertiesExampleViewModel);
    }
  3. Apply the WebBinding on the page:
    @Html.WebBinder(bd)

Since we use the Knockout.js library for binding between the JavaScript view-model and the HTML elements, we have to apply the Knockout binding too:

<script type="text/javascript" src="~/Scripts/knockout-3.0.0.js"></script>

<script type="text/javascript">
    // ...

    function ExampleViewModel() {
        var self = this;
        this.uniqueVm = uniqueVm;
        this.sharedVm = sharedVm;
    }

    var viewModel = new ExampleViewModel();
</script>

<script type="text/javascript">
    ko.applyBindings(viewModel);
</script>

For presenting our example, we add 2 input tags that presents the bound Text property (one for the shared view-model and, one for the unique view-model):

<section>
    <h3>Example 1: Shared view-model vs. unique view-model</h3>
    <p class="exampleDescription">In this example, we compare between shared (with the other pages) view-model and, unique (to this page) view-model.
        We can see how the change on the shared view-model is reflected to the other pages (open this page in some tabs/windows),
        while the change on the unique view-model stays unique to that page.</p>
    <h4>Shared view-model</h4>
    <p>
        Text: <input type="text" data-bind="value: sharedVm.text"/> - 
        Entered value: <span style="color :blue" data-bind="text: sharedVm.text"></span>
    </p>
    <h4>Unique view-model</h4>
    <p>
        Text: <input type="text" data-bind="value: uniqueVm.text"/> - 
        Entered value: <span style="color :blue" data-bind="text: uniqueVm.text"></span>
    </p>
</section>

The result is:

Example 1

Example 2: 2 Dimensional Collection

In the second example, we present a web-bound 2 dimensional collection of integers. The collection's values are updated randomly by the server (and reflected to the whole of the clients).

For holding our collection, we add an appropriate property to our view-model:

private ObservableCollection<ObservableCollection<int>> _numbersBoard;

[WebBound(ClientPropertyName = "numbersBoard")]
public ObservableCollection<ObservableCollection<int>> NumbersBoard
{
    get { return _numbersBoard; }
    set
    {
        if (_numbersBoard != value)
        {
            _numbersBoard = value;
            NotifyPropertyChanged("NumbersBoard");
        }
    }
}

For changing the collection's dimensions from the client side, we add 2 more properties:

#region NumbersBoardRowsCount

private int _numbersBoardRowsCount;

[WebBound(ClientPropertyName = "numbersBoardRowsCount")]
public int NumbersBoardRowsCount
{
    get { return _numbersBoardRowsCount; }
    set
    {
        if (_numbersBoardRowsCount != value)
        {
            lock (_numbersBoard)
            {
                while (_numbersBoard.Count > value)
                {
                    _numbersBoard.RemoveAt(value);
                }

                while (_numbersBoard.Count < value)
                {
                    ObservableCollection<int> newRow = new ObservableCollection<int>();
                    for (int colInx = 0; colInx < NumbersBoardColumnsCount; colInx++)
                    {
                        newRow.Add(colInx);
                    }

                    _numbersBoard.Add(newRow);
                }
            }

            _numbersBoardRowsCount = value;
            NotifyPropertyChanged("NumbersBoardRowsCount");
        }
    }
}

#endregion

#region NumbersBoardColumnsCount

private int _numbersBoardColumnsCount;

[WebBound(ClientPropertyName = "numbersBoardColumnsCount")]
public int NumbersBoardColumnsCount
{
    get { return _numbersBoardColumnsCount; }
    set
    {
        if (_numbersBoardColumnsCount != value)
        {
            if (_numbersBoardColumnsCount > value)
            {
                lock (_numbersBoard)
                {
                    foreach (var row in NumbersBoard)
                    {
                        while (row.Count > value)
                        {
                            row.RemoveAt(value);
                        }
                    }
                }
            }

            if (_numbersBoardColumnsCount < value)
            {
                lock (_numbersBoard)
                {
                    foreach (var row in NumbersBoard)
                    {
                        while (row.Count < value)
                        {
                            row.Add(row.Count);
                        }
                    }
                }
            }

            _numbersBoardColumnsCount = value;
            NotifyPropertyChanged("NumbersBoardColumnsCount");
        }
    }
}

#endregion

When the values of those properties are changed, we update the collection's dimensions appropriately.

In order to demonstrate how the server's changes are reflected to the clients, we add a timer that changes the collection's values randomly after elapsed intervals:

private System.Timers.Timer _nbTimer;

private void StartNumbersBoardTimer()
{
    _nbTimer = new System.Timers.Timer();
    _nbTimer.AutoReset = true;
    _nbTimer.Interval = 1000;
    _nbTimer.Elapsed += OnNbTimerElapsed;
    _nbTimer.Start();
}

private void OnNbTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    lock (_numbersBoard)
    {
        Random r = new Random((int)DateTime.Now.Ticks);
                
        for (int rowInx = 0; rowInx < _numbersBoard.Count; rowInx++)
        {
            ObservableCollection<int> currRow = _numbersBoard[rowInx];
            for (int colInx = 0; colInx < currRow.Count; colInx++)
            {
                currRow[colInx] = r.Next(0, 100);
            }
        }
    }
}

private void StopNumbersBoardTimer()
{
    if (_nbTimer != null)
    {
        _nbTimer.Stop();
        _nbTimer = null;
    }
}

For presenting our example, we add 2 input tags for setting the collection's dimensions and, a table for presenting the collection:

<section>
    <h3>Example 2: 2 dimensional collection</h3>
    <p class="exampleDescription">In this example, we change the columns' number and the rows' number of a 2D collection.
        In addition to that, the cells' values are changed randomly by the server. 
        We can see how the values are synchronized with the other pages.</p>
    <p>   
        Rows count: <input type="text" data-bind="value: sharedVm.numbersBoardRowsCount"/> - 
        Entered value: <span style="color :blue" data-bind="text: sharedVm.numbersBoardRowsCount"></span>
        <br /> 
        Columns count: <input type="text" data-bind="value: sharedVm.numbersBoardColumnsCount"/> - 
        Entered value: <span style="color :blue" data-bind="text: sharedVm.numbersBoardColumnsCount"></span>
        <br /> 
    </p>
            
    <table style="background:lightgray;border:gray 1px solid;width:100%">
        <tbody data-bind="foreach: sharedVm.numbersBoard">
            <tr data-bind="foreach: $data">
                <td style="background:lightyellow;border:goldenrod 1px solid">
                    <span style="color :blue" data-bind="text: $data"></span>
                </td>
            </tr>
        </tbody>
    </table>
</section>

The result is:

Example 2

Example 3: String as a Collection

In the third example, we present a web-bound string in two ways: as a string and, as a collection of characters.

For holding our string, we add 2 web-bound properties:

#region StringEntry

private string _stringEntry;

[WebBound]
public string StringEntry
{
    get { return _stringEntry; }
    set
    {
        if (_stringEntry != value)
        {
            _stringEntry = value;
            NotifyPropertyChanged("StringEntry");
            NotifyPropertyChanged("StringEntryCharacters");
        }
    }
}

#endregion

#region StringEntryCharacters

[WebBound(IndicateCollectionTypeAutomatically = false, IsCollection = true)]
public string StringEntryCharacters
{
    get { return _stringEntry; }
}

#endregion

The StringEntry property encapsulates the value of the _stringEntry field. The StringEntryCharacters property returns the same value but, is exposed as a collection (using the appropriate properties of the WebBound attribute).

Note that in the other examples, we use the ClientPropertyName property for setting the client-side's property's name. In order to demonstrate the default behavior (give the client-side property the same name as the server-side property), in this example, we omit the use of the ClientPropertyName property.

For presenting our example, we add an input tag for presenting our string as a string and, a table for presenting our string as a characters' collection:

<section>
    <h3>Example 3: String as a collection</h3>
    <p class="exampleDescription">In this example, we show a string as a collection of characters.</p>  
    <h4>The string</h4>              
    <p>
        StringEntry: <input type="text" data-bind="value: sharedVm.StringEntry"/> -  
        Entered value: <span style="color :blue" data-bind="text: sharedVm.StringEntry"></span>
    </p>
    <h4>The string's characters</h4>
    <table style="background:lightgray;border:gray 1px solid;width:100%">
        <tbody>
            <tr data-bind="foreach: sharedVm.StringEntryCharacters">
                <td style="background:lightyellow;border:goldenrod 1px solid">
                    <span style="color :blue" data-bind="text: $data"></span>
                </td>
            </tr>
        </tbody>
    </table>
</section>

The result is:

Example 3

Example 4: Change Collections from the Client Side

In the fourth example, we present a web-bound collections of a more complex (than simple types like string, int, etc...) type. We can add or remove items from these collections in the client side (and, see how the changes are reflected to the other clients).

For the collection's element type, we create a view-model that presents a person:

public class PersonViewModel : BaseViewModel
{
    public PersonViewModel()
    {
        Children = new ObservableCollection<PersonViewModel>();
    }

    #region Name

    private FullNameViewModel _name;

    [WebBound(ClientPropertyName = "name")]
    public FullNameViewModel Name
    {
        get { return _name; }
        set
        {
            if (value != _name)
            {
                _name = value;
                NotifyPropertyChanged("Name");
            }
        }
    }

    #endregion

    #region Age

    private int _age;

    [WebBound(ClientPropertyName = "age")]
    public int Age
    {
        get { return _age; }
        set
        {
            if (value != _age)
            {
                _age = value;
                NotifyPropertyChanged("Age");
            }
        }
    }

    #endregion

    #region Children

    private ObservableCollection<PersonViewModel> _children;

    [WebBound(ClientPropertyName = "children")]
    public ObservableCollection<PersonViewModel> Children
    {
        get { return _children; }
        set
        {
            if (_children != value)
            {
                _children = value;
                NotifyPropertyChanged("Children");
            }
        }
    }

    #endregion
}

public class FullNameViewModel : BaseViewModel
{
    #region FirstName

    private string _firstName;

    [WebBound(ClientPropertyName = "firstName")]
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            if (value != _firstName)
            {
                _firstName = value;
                NotifyPropertyChanged("FirstName");
            }
        }
    }

    #endregion

    #region LastName

    private string _lastName;

    [WebBound(ClientPropertyName = "lastName")]
    public string LastName
    {
        get { return _lastName; }
        set
        {
            if (value != _lastName)
            {
                _lastName = value;
                NotifyPropertyChanged("LastName");
            }
        }
    }

    #endregion
}

For holding our collection, we add a web-bound property to our view-model:

private ObservableCollection<PersonViewModel> _people;

[WebBound(ClientPropertyName = "people")]
public ObservableCollection<PersonViewModel> People
{
    get { return _people; }
    set
    {
        if (_people != value)
        {
            _people = value;
            NotifyPropertyChanged("People");
        }
    }
}

In order to create new collection's elements in the client side, in a way that their changes will be reflected to the server, we have to create objects that have registrations for their properties' changes. For that purpose, we expose a function that creates a web-bound object:

@{
    BinderDefinitions bd = new KnockoutBinderDefinitions();
    bd.CreateBoundObjectFunctionName = "createWebBoundObject";

    // ...
}

For enabling adding or removing collection's elements in the client side, we add appropriate functions that perform those actions:

function ExampleViewModel() {
    // ...

    // Actions for example 4.
    this.removePerson = function (person) {
        var peopleArr = self.sharedVm.people();

        var foundIndex = -1;
        for (var personInx = 0; personInx < peopleArr.length && foundIndex < 0; personInx++) {
            if (peopleArr[personInx]() == person) {
                foundIndex = personInx;
            }
        }

        if (foundIndex >= 0) {
            peopleArr.splice(foundIndex, 1);
        }

        self.sharedVm.people(peopleArr);
    };

    this.removeChild = function (child) {
        var peopleArr = self.sharedVm.people();

        var foundIndex = -1;
        for (var personInx = 0; personInx < peopleArr.length && foundIndex < 0; personInx++) {
            var childrenHolder = peopleArr[personInx]().children;
            var childrenArr = childrenHolder();

            for (var childInx = 0; childInx < childrenArr.length && foundIndex < 0; childInx++) {
                if (childrenArr[childInx]() == child) {
                    foundIndex = childInx;
                }
            }

            if (foundIndex >= 0) {
                childrenArr.splice(foundIndex, 1);
                childrenHolder(childrenArr);
            }

        }
    };

    this.addPerson = function () {
        var peopleArr = self.sharedVm.people();
        var newIndex = peopleArr.length;
        var propPath = "people[" + newIndex + "]";
        var person = createWebBoundObject(self.sharedVm, propPath);
        person().name().firstName("Added_First" + (newIndex + 1));
        person().name().lastName("Added_Last" + (newIndex + 1));
        person().age(40 + newIndex);
        peopleArr.push(person);
        self.sharedVm.people(peopleArr);
    };

    this.addChild = function (person) {
        // Find person's index.
        var peopleArr = self.sharedVm.people();
        var foundIndex = -1;

        for (var personInx = 0; personInx < peopleArr.length && foundIndex < 0; personInx++) {
            if (peopleArr[personInx]() == person) {
                foundIndex = personInx;
            }
        }

        // Add child to the found person.
        if (foundIndex >= 0) {
            var childrenHolder = peopleArr[foundIndex]().children;
            var childrenArr = childrenHolder();
            var newIndex = childrenArr.length;
            var propPath = "people[" + foundIndex + "].children[" + newIndex + "]";
            var child = createWebBoundObject(self.sharedVm, propPath);
            child().name().firstName("Added_First" + (foundIndex + 1) + "_" + (newIndex + 1));
            child().name().lastName("Added_Last" + (foundIndex + 1) + "_" + (newIndex + 1));
            child().age(20 + newIndex);
            childrenArr.push(child);
            childrenHolder(childrenArr);
        }
    };
}

For presenting our example, we add a list for presenting our collection and, buttons for applying the appropriate actions:

<section>
    <h3>Example 4: Change collections from the client side</h3>
    <p class="exampleDescription">In this example, we add and remove collection's elements (from the client side).
        We can see how the changes are reflected to the other pages.</p>
    <h4>People collection</h4>
    <ol data-bind="foreach: sharedVm.people">
        <li>Name: <span style="color :blue" data-bind="text: name().firstName"></span> 
            <span style="color :brown">, </span>
            <span style="color :blue" data-bind="text: name().lastName"></span>
            Age: <span style="color :blue" data-bind="text: age"></span>
            <button data-bind="click: $root.removePerson">Remove</button>
            <br />
            Children:
            <ol data-bind="foreach: children">
                <li>
                    Name: <span style="color :blue" data-bind="text: name().firstName"></span> 
                    <span style="color :brown">, </span>
                    <span style="color :blue" data-bind="text: name().lastName"></span>
                    Age: <span style="color :blue" data-bind="text: age"></span>
                    <button data-bind="click: $root.removeChild">Remove</button>
                </li> 
            </ol>
            <button data-bind="click: $root.addChild">Add child</button>
        </li>
    </ol>
    <button data-bind="click: $root.addPerson">Add person</button>
</section>

The result is:

Example 4

License

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

About the Author

Shmuel Zang
Software Developer
Israel Israel
No Biography provided
Follow on   LinkedIn

Comments and Discussions

 
QuestionHaving looked at this article a bit more now PinmvpSacha Barber2-May-14 1:06 
AnswerRe: Having looked at this article a bit more now PinpremiumShmuel Zang5-May-14 21:35 
GeneralMy vote of 5 PinpremiumPrasad Khandekar4-Apr-14 2:53 
GeneralRe: My vote of 5 PinpremiumShmuel Zang5-Apr-14 20:45 
GeneralMy vote of 5 PinmvpRahul Rajat Singh3-Apr-14 1:43 
GeneralRe: My vote of 5 PinpremiumShmuel Zang3-Apr-14 5:07 
GeneralMy vote of 5 PinpremiumVolynsky Alex22-Mar-14 8:53 
GeneralRe: My vote of 5 PinpremiumShmuel Zang22-Mar-14 20:14 
GeneralRe: My vote of 5 PinpremiumVolynsky Alex22-Mar-14 23:03 
GeneralMy vote of 5 PinmemberJaredThirsk21-Mar-14 22:32 
GeneralRe: My vote of 5 PinpremiumShmuel Zang22-Mar-14 20:12 

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.140721.1 | Last Updated 1 May 2014
Article Copyright 2014 by Shmuel Zang
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid