Saturday, 5 March 2016

Lightning Components,Visualforce and SObject Parameters

Lightning Components,Visualforce and SObject Parameters

Introduction

This week’s post concerns the original issue that I mentioned in last week’s post, Lightning Component Events - Change Types with Care. First a bit of background - I first saw this issue manifest itself in our BrightMedia demonstration community. The community home page lists items of premium inventory that are still available:

Screen Shot 2016 03 05 at 11 43 02

and users can click the ‘Book’ button to place an advert in that space. Clicking the button opens the community booking page, which is composed of a number of Lightning Components (each of which is composed of Lightning Components, and so on, a bit like Russian dolls). These are the same components that are used in our mobile application, except that there they appear individually as part of a booking wizard. As we weren’t using the community builder, but Visualforce pages and custom objects, the components are surfaced via Visualforce as detailed in LDS Activity Timeline, Lightning Components and Visualforce :

Screen Shot 2016 03 05 at 11 45 34

The user then chooses the date on which they would like to place the ad, and clicks the ‘Pay and Confirm’ button to make an online payment and commit the booking.

The Problem

All this has been working fine for some time, since at least Dreamforce 2015 which is where we first demonstrated it, but as of the Spring 16 release we started seeing the following error:

Screen Shot 2016 03 05 at 11 57 53

The Solution (altogether now : Oh No It Isn’t)

After spending some time debugging this, I came to the (erroneous, it turned out) conclusion that the issue was caused by a bonus $serId attribute that appeared on one of the sobject instances that is being sent back to the server. Stripping this attribute via a JSON stringify replacer fixed it in this specific case. However, we then started seeing it when building new functionality, where there were no bonus attributes, just those that matched the sobject fields.

Reproduce It and You Can Fix It

The difficulty I had tracking down what was going on in the community booking page was the sheer number of components and amount of JavaScript to wade through - while booking an ad might sound simple, there’s several thousand lines of code behind it to handle all the edge cases. What I needed was to be able to reproduce the problem in a simple test case, especially if I wanted to raise a case with Salesforce support. To achieve this I built a simple example component that retrieved an Account when the user clicked a button and stored it as an attribute in the component. Then when the user clicked another button, retrieved the Account and sent it back to the server. This of course worked perfectly so it was back to the drawing board. 

The component:

 <aura:component controller="bg_SObjectTestCtrl">
    <aura:attribute name="acc" type="Account" />

    <button onclick="{!c.getFromServer}">Get the Account</button><br/>
    Account Name : {!v.acc.Name}<br/>
    <button onclick="{!c.sendToServer}">Send the Account</button>
</aura:component>

The component controller:

({
    getFromServer : function(component, event, helper) {
        helper.getFromServer(component, event);
    },
    sendToServer : function(component, event, helper) {
        helper.sendToServer(component, event);
    }
})

(I know I could have put all the logic into the controller, but I delegated to the helper as that is how my real application that exhibited the error worked, and you never know).

The component helper:

({
    getFromServer : function(cmp, ev) {
        var action=cmp.get('c.GetAccount');
        var self=this;
        action.setCallback(this, function(response) {
                self.actionResponseHandler(response, cmp, self, self.gotAccount);
        });
        $A.enqueueAction(action);
    },
    gotAccount : function(cmp, helper, acc) {
        cmp.set('v.acc', acc);
    },
    sendToServer : function(cmp, ev) {
        var action=cmp.get('c.SendAccount');
        var acc=cmp.get('v.acc');
        action.setParams({'acc' : acc});
        var self=this;
        action.setCallback(this, function(response) {
                self.actionResponseHandler(response, cmp, self, self.sentAccount);
        });
        $A.enqueueAction(action);
    },
    sentAccount : function(cmp, helper, acc) {
        alert('Sent account to server');
    },
    actionResponseHandler : function (response, component, helper, cb, cbData) {
        try {
            var state = response.getState();
            if (state === "SUCCESS") {
                var retVal=response.getReturnValue();
                cb(component, helper, retVal, cbData);
            }
            else if (state === "ERROR") {
                var errors = response.getError();
                if (errors) {
                    if (errors[0] && errors[0].message) {
                        alert("Error message: " + errors[0].message);
                    }
                }
                else {
                    alert("Unknown error");
                }
            }
        }
        catch (e) {
            alert('Exception in actionResponseHandler: ' + e);
        }
    }
})

and finally the Apex controller:

public class bg_SObjectTestCtrl {
    @AuraEnabled
    public static Account GetAccount()
    {
        Account acc=new Account(Name='Test Account');

        return acc;
    }

    @AuraEnabled
    public static void SendAccount(Account acc)
    {
        System.debug('Account = ' + acc);
    }
}

On the community booking page, the booking is passed around many of the components via event attributes, so I decided this was the likely candidate, and added another component and associated event to my attempt to reproduce, sending the Account between the components via events. Aside from breaking my deployment when I changed the component type without changing the rest of the markup, this attempt fared no better.

Its In the Visualforce

After further digging around and discussions, I realised that the common denominator was that the components were embedded in a Visualforce page, so I had another avenue to explore.

The Visualforce page:

<apex:page sidebar="false" showHeader="false" standardStylesheets="false">
    <apex:includeLightning />
    <div id="lightning"/>

    <script>
        $Lightning.use("c:SObjectTestApp", function() {
            $Lightning.createComponent("c:SObjectTest",
                  {},
                  "lightning",
                  function(cmp) {
                    // any additional processing goes here
              });
        });
    </script>
</apex:page>

The lightning application that makes the component accessible via Visualforce:

<aura:application access="GLOBAL" extends="ltng:outApp">
    <aura:dependency resource="c:SObjectTest" />
</aura:application>

Sadly this gave the same results - everything worked fine and there were no errors.

What’s In An SObject

In what was starting to feel like a final roll of the dice, I examined the sobjects that were causing the issue on the booking page. One characteristic that they had that my sample component Account didn’t was one or more populated relationship fields (lookup or master-detail) fields.

I changed my Apex controller to populate the ParentId on the Account :

public static Account GetAccount()
{
    List<Account> accounts=[select id from Account limit 1];
    Account acc=new Account(Name='Test Account',
                             ParentId=accounts[0].id);

    return acc;
}

This did the trick and started throwing the error. So now I had a simple example that exhibited the same issues as my complex page.

The Solution (altogether now: Oh Yes It Is!)

After verifying that there was nothing untoward going on with my account, no bonus attributes, I set about trying to find a way to get the Account recognised as an sobject by the framework. I had a vague recollection of reading something from Doug Chasman around how some components needed a helping hand to identify the type of sobject they were dealing with, and a default instance containing the sobjectType attribute was the recognised way to achieve this. Checking my sobjects, I could see that this attribute wasn’t set when the Account was sent through, so I changed my sample component helper method that sends the account to the server accordingly:

sendToServer : function(cmp, ev) {
    var action=cmp.get('c.SendAccount');
    var acc=cmp.get('v.acc');

    // set the sobjectType!
    acc.sobjectType='Account';

    action.setParams({'acc' : acc});
    var self=this;
    action.setCallback(this, function(response) {
            self.actionResponseHandler(response, cmp, self, self.sentAccount);
    });
    $A.enqueueAction(action);
}

and this fixed the problem. I then applied the same fix to my community booking page and that also started working. Quite why the Lighting platform can’t recognise an Account when there is a lookup field populated, but can with only the Id and Name to work with is anyone’s guess, but there you have it!

My booking page sobjects still had the $serId bonus attribute by the way, so that turned out to be a total red herring!

Related Posts

 

1 comment:

  1. A smallish campaign with a homemade list would not be likely to yield much of a result. To achieve anything worthwhile, a much more aggressive effort is needed. Then, the age-old value analysis applies: projected earnings = margin on total projected sales - cost of campaign.

    ReplyDelete