HTML Client Computed Properties

It would be nice and fairly simple for MS to add Computed Properties to the HTML Client. In fact if they'd just include them in the model then we could do it ourselves.

Rather than hold our breath, let's make it happen.

Here's one way to make Computed Properties easy - and it requires zero change to msls.js.

1) Add an optional property to your entity. I have Customer and added a property called 'CSZ' - required=false.

2) In the entity designer select HTML perspective and choose Write Code | Created. This adds the entity.js client side script. (customer.lsml.js in my case)

3) Now write _Compute methods in your entity.js file (Customer.lsml.js for example) like so:

myapp.Customer.created = function (entity) {  
    // Write code here.

};

myapp.Customer.CSZ_Compute = function (entity, result) {  
    // Write code here.
    var Me = entity;
    result = Me.City + ', ' + Me.State + "  " + Me.Zip;
    return result;
};

Notice how similar this is to SL compute methods.

4) Create a new script called OdataReadPatch.js in your scripts folder and paste in the this code. Also add a reference to this script in your default.htm after all other script refs.

/*
This script patches the OData read method  
enabling us to inject operations before the results return to the LS runtime  
*/
var origJsonReadFunc = OData.jsonHandler.read;  
OData.jsonHandler.read = function (response, context) {  
    var result = origJsonReadFunc.call(OData.jsonHandler, response, context);
    var data = response.data, results = data.results;
    if (results) {
        results.forEach(function (entity) {
            //do stuff to each entity here

            //call function to add Computed properties
            myapp.addComputedProperties.call(this, entity);

        });
    }

    return result;
};

myapp.addComputedProperties = function (entity) {

    //get entity name  ie: "LightSwitch.Customer" = "Customer"
    var entityType = entity.__metadata.type.split(".").pop();

    //get the entity class - this object contains all methods defined in Customer.lsml.js
    var entityClass = myapp[entityType]

    //build an array of property names from '_Compute' methods
    var properties = [];
    for (var p in entityClass) {
        if (typeof entityClass[p] === "function" && p.indexOf("_Compute") > 0) {
            prop = { name: p.split("_")[0], type: String };
            properties.push(prop);
        }
    };
    //console.log(properties);

    //add the computed prop to this entity by calling the _Compute method
    if (properties) {
        properties.forEach(function (entry) {
            var entryName = entry.name;
            var computeMethod = entityClass[entryName + "_Compute"];
            entity[entryName] = computeMethod.call(this, entity);
        });
    }
};

or download OdataReadPatch.js from here:

https://gist.github.com/joshbooker/ddb4b07683e32a068d5d

Done!

Now just add the property to a browse screen and it works.

After this is setup, adding new computed props is as simple as adding a _Compute methods.

The trick is the Odata patch which allows us to inject an operation to add the properties to each entity before the results are returned to LS runtime.

More info:

The idea was to make HTML Client computed properties nearly the same as SL. My understanding is that the compute methods in SL is on the client - or at least both client and server. The missing part on the HTML client was until they added the entitycreated method, there was no client side table code to mirror that in the .net client.

LS defines entities using the generated model file data.js which cannot be changed and computed properties are not included. As a result, we typically use existing entry points to add them such as _render & _postRender. These are specific to code in a single screen and for a single contentItem on that screen. Thus there is a ton of code duplication for a computed prop that appears on many screens or many times on a single screen. Also, to keep the computed property current, dataBind code is needed. For a simple computation like 'CSZ' above, which references 3 other properties (City, State & Zip), at least 3 dataBinds are needed. Need to change a calculation? - Make sure to do it here and here and here and there and there, you get the picture. With this solution, the calculated property is added to the data - no the screen, computation code is written in one place and dataBind is automatic.
How does it work?

This solution adds properties to the entityData returned from each Odata Read call. With these props present, LS builds the entities and the properties are majically added to the Entity Details just as though they were in the model to begin with. So LS runtime doesn't appear to 'know' they are any different that another field in the data.

The collection of computed properties to be added is derived from looking directly at the client side entity code (.lsml.js) and finding all methods ending in 'Compute'. It is assumed that those methods will accept entityData and return the computed result string. The name of the property is the portion of the methid name before _Compute ( ie: 'myapp.Customer.CSZCompute(entity)' adds a computed property called 'CSZ' to all Customer entities). Therefore all you have to do to add a computed prop is add another _Compute method to your entity code. The property is added even if you don't add one of the same name in the entity designer and is therefore available for _render and dataBind manually.

Optionally, If you do add a property by the same name to the entity designer, you can use it on screens and LS will render and bind the computed value on screens automatically - no _render/dataBind necessary. That part was a shocker to me.

Because this computes the props on every OData call, the values are recomputed quite often. There's barely a need for dependency changes listeners to auto-recompute like SL does.

To summarize,
  • You create these computed props in one step
  • The computation exists in one and only one place.
  • By optionally adding a prop to the model and views, LS will do the dataBinding for you.
  • They re-compute with every Odata call
  • Zero change to msls.js.

I think this hook may be the ticket to crack the data layer for a lot of other benefits.

Have a great day!

Josh Booker

Read more posts by this author.