MVVM in WinJS Part 3 – Binding Initializers and Two Way Binding

.NET Musings

Wandering thoughts of a developer, architect, speaker, and trainer

NAVIGATION - SEARCH

MVVM in WinJS Part 3 – Binding Initializers and Two Way Binding

A Quick Status Check

I started this blog series on developing Windows 8 Store Applications with JavaScript to document my process of learning WinJS and Windows 8 as well as the functionality that would be needed for Conference Buddy – an application that I am writing for Windows 8 with my good friend Jesse Liberty. (Note: Michael Crump is writing a version for Windows Phone – you should check that out as well).

I haven’t spent much time writing specifically about Conference Buddy to date since the posts that I have written so far have been much broader and foundational for developing WinJS applications.  In time, I will outline how I used each one of these concepts to create Conference Buddy from start to finish.

Here are the posts that are already published in this series:

Background

A lot had been said about the Windows 8 data binding mechanism and its lack of support for two way binding.  While that is inherently true, it isn’t the whole story. You can certainly use other JavaScript frameworks (like Knockout), but if you want to stick to the “out of the box” tooling, you must listen to an element’s change event and then update the source object.  Of course, support for two way binding out of the box would be better, and the Windows team is very aware of that. 

As I am sure you are well aware, data binding is done with the data-win-bind attribute and the “property-to-bind:value-from-data-context” syntax.  This provides model to view binding. The data-win-bind attribute also supports another parameter, and that is a binding initializer.  Through the binding initializer, you can incorporate view to model binding in a very MVVM way.  To use a binding initializer in markup, set the data-win-bind as in the following code snippet:

data-win-bind=”property-to-bind:value-from-data-context bindingInitializer”

Creating a Binding Initializer

To create a binding initializer by hand, start by wrapping a function in a call to WinJS.Binding.initializer (as in Listing 1).  WinJS.Binding.initializer adds a call to WinJS.Utilities.markSupportedForProcessing, which allows using the function declaratively in markup (as the third parameter in the binding expression as explained above). 

WinJS.Namespace.define("ConferenceBuddy.Binding", {
    twoWay: WinJS.Binding.initializer(function (
              source, sourceProps, dest, destProps) {
    })
});
Listing 1 – Creating a Binding Initializer

The binding initializer in a binding expression is passed in the source object, an array of the source properties, the destination object, and an array of the destination properties.  The source is the model object that is being bound to (the data context), the property string array contain properties listed in the the “value-from-data-context”, the destination is the element in markup (either an HTML element or a WinJS control), and the destination property array are the properties of the markup element/WinJS controls that is being modified.

It The Binding Initializer only gets called once (when the binding is getting set up after a call to WinJS.Binding.processAll), so it’s important to return a binding or create an event handler that will then respond to changes in the data values. 

Attaching an Event Listener

To update the source object (your model), you need to listen for the change event on your view element.  When the change is fired, compare the value of the element with the value of the model, and if they are different, update the model.  The comparison is important, because you don’t want to cause a recursion issue.  The code is shown in Listing 2.  I will detail each of the functions used later in this post.

WinJS.Namespace.define("ConferenceBuddy.Binding", {
  twoway: WinJS.Binding.initializer(function (
                 source, sourceProps, dest, destProps) {
    dest.onchange = function () {
        var newValue = ConferenceBuddy.Binding.Helpers.
                         dest, destProps);
        var targetObject = ConferenceBuddy.Binding.Helpers
.getSourceObject(source, sourceProps);
        var oldValue = targetObject[sourceProps[sourceProps.length - 1]];
        if (oldValue !== newValue) {
            targetObject[sourceProps[sourceProps.length - 1]] = newValue;
        }
    };
  })
});

Listing 2 – Attaching an Event Listener in a Binding Initializer

Getting the New Value

The last two parameters contain the view element and the properties.  If you are doing simple binding like “value:age”, then getting the value is as simple as writing dest[destProps[0]].  However, real life is rarely that simple.  If you are binding to a JavaScript control, then the construct will fail, because the value is a property of the winControl and not the element.  So you would then need to write dest.winControl[destProps[1]].  If you are binding to a path, such as “style.color:age”, then the first value in the array with be “style”, and you won’t get the desired effect either.

So there’s two cases that we need to program for:

  • The simple case where the destProps array only hold one vale
  • Any other case where there are more than 1 properties.

Listing three shows the implementation of the getDistinationValue function.

WinJS.Namespace.define("Binding.Helpers", {
    getDestinationValue: function (dest, destProps) {
        if (destProps.length === 1) {
                return dest[destProps[0]];
        }
        else {
            var element = eval("dest['" + 
                  destProps.slice(0, destProps.length - 1).join("']['") + 
                  "']");
            return element[destProps[destProps.length-1]];
        }
    },
});
Listing 3 – Getting the value from the view

Slice, Join, and Eval

The actual property that needs to be read is always the last leaf in the array.  The construct that will get us the element is dest[destProps[0]][destProps[1]][destProps[n-1]].  Instead of iterating through the array, we use a JavaScript tricks to get a reference to the element.  The “join” function will join an array of strings into a single string with the option to specify the connector for the strings.  In this case, we want to build the string to look like “[‘property1’][‘property2’]…[‘property(n-1)’].  And since we want the element, and not the property itself, we only want to build the string using all but the last element of the array.  We get the desired items from the array by using the slice method on the array starting with zero (the first item) and then getting all but the last one by passing in one less than the length of the array.  The slice method returns a new array, so we can directly call join after slice, and get the desired string.  Then a call to eval processes the string into the element.  The final line returns the value of the property that we are seeking.

NOTE: If you are using the Telerik RadControls for WinJS, this becomes much easier, as the WinJS team at Telerik has created two utility functions, Telerik.Utilities.getPropertyValue(dest, destProps) and Telerik.Utilities.setPropertyValue(dest, destProps, value)

Getting and Updating the Old Value From the Model

Retrieving the old value follows the same process of using the slice, join, and eval functions to get the model object.  Since the event handler potentially needs to update the value, the function returns the object and not just the value. Once we have the object, we can get or set the value of the property by using the element[sourceProps[sourceProps.length-1]] syntax.

getSourceObject: function (source, sourceProps) {
    if (sourceProps.length === 1) {
        return source;
    }
    else {
        return eval("source['" + sourceProps
            .slice(0, sourceProps.length - 1)
            .join("']['") + "']");
    }
},
Listing 4 – Getting Source Object

The Result

To test the two way binding, I set up a screen with a record displayed on the left and the same record on the right with text boxes for the names and a range control bound to the age property.  Changing the value of the range control causes the values for the age in the label and the textbox to be immediately updated.  Likewise, changing the value in the text box updates the value of the range control and the label.

image

Summary

Although it might seem like a lot of extra work, it really just takes longer to explain than code.  Two way binding is very achievable in WinJS through just a few lines of code.   I’ve added this to my JustCode JavaScript templates so I always have it at my finger tips.  Yes, out of the box support for two way binding would be much better, and yes, you can pull in any framework you want to handle binding for you.  But you can also do with the tooling provided out of the box by taking advantage of the Binding Initializers.

About the author

Philip Japikse

Philip Japikse Philip Japikse is an international speaker, a Microsoft MVP, INETA Community Champion, MCSD, CSM/ CSP, and a passionate member of the developer community, Phil has been working with .Net since the first betas, developing software for over 20 years, and heavily involved in the agile community since 2005. Phil works as a Developer Evangelist for Telerik's RadControls for Windows 8 as well as the Just family of products (JustCode, JustMock, JustTrace, and JustDecompile) and co-hosts the Hallway Conversations podcast (www.hallwayconversations.com). Phil is also the Lead Director for the Cincinnati .Net User’s Group (http://www.cinnug.org). You can follow Phil on twitter via www.twitter.com/skimedic read his Telerik blog at http://blogs.telerik.com/skimedic and his personal blog at www.skimedic.com/blog.

Pingbacks and trackbacks (2)+

Comments are closed
Managed Windows Shared Hosting by OrcsWeb