Tuesday, July 16, 2013

SharePoint 2013 Apps: “Invalid JSON Data” JavaScript exception when loading the Chrome Control declaratively

SharePoint 2013 introduces the new app model making it possible to integrate externally hosted apps into SharePoint. One of the features of the new app framework is the so called “Chrome Control” which enables the externally hosted apps to dynamically load the SharePoint CSS and thus implement the same look and feel as SharePoint. There is a nice example on how to use the Chrome Control at http://msdn.microsoft.com/en-us/library/fp179916.aspx, so I will not go into the basics here. Rather, I want to point you to a nasty bug in the current SharePoint 2013 version which might affect you if you are loading the Chrome Control declaratively.

Note: you only need the Chrome Control in your provider or autohosted apps that run outside of the SharePoint context. You don’t need the Chrome Control in the SharePoint hosted apps since those run in the SharePoint context and can reference the necessary styles by referencing the SharePoint master page.

A little bit of background first. There are two ways to load the Chrome Control into your app: the JavaScript way and the declarative way.

The JavaScript way to load the Chrome Control looks something like this: first you need a page in your app with a placeholder that will host the Chrome Control once it’s loaded, e.g. (removed details for simplicity):

...
<body style="display: none">
    <form id="form1" runat="server">
        <!-- Chrome control placeholder -->
        <div id="chrome_ctrl_container"></div>
 
        <!-- The chrome control also makes the SharePoint
          Website stylesheet available to your page -->
        <h1 class="ms-accentText">Main content</h1>
        <div id="MainContent">
 
        </div>
    </form>
</body>
...

In this case I have a <div id=”chrome_ctrl_container”> tag where the Chrome Control will be loaded. Once you have this, you can easily load the Chrome Control using JavaScript like this:



   1: var SPWorkshop = window.SPWorkshop || {};
   2:  
   3: SPWorkshop.ChromeControl = function () {
   4:  
   5:     var render = function () {
   6:         "use strict";
   7:         // We are using document.URL.split("?")[1] to pass the original query string parameters
   8:         // to all other pages. Note that this is currently possible only when using JavaScript
   9:         // to render Chrome control, the declarative rendering has a bug which prevents the 
  10:         // document.URL.split("?")[1] parts to be interpreted correctly.
  11:         var options = {
  12:             "appIconUrl": "../Images/AppIcon.png",
  13:             "appTitle": "SharePoint 2013 App",
  14:             "appHelpPageUrl": "../Pages/Help.aspx?" + document.URL.split("?")[1],
  15:             // The onCssLoaded event allows you to 
  16:             //  specify a callback to execute when the
  17:             //  chrome resources have been loaded.
  18:             "onCssLoaded": "SPWorkshop.ChromeControl.chromeLoaded()",
  19:             "settingsLinks": [
  20:                 {
  21:                     "linkUrl": "../Pages/Contacts.aspx?" + document.URL.split("?")[1],
  22:                     "displayName": "Contacts"
  23:                 },
  24:                 {
  25:                     "linkUrl": "../Pages/Welcome.aspx?" + document.URL.split("?")[1],
  26:                     "displayName": "Welcome"
  27:                 }
  28:             ]
  29:         };
  30:  
  31:         var nav = new SP.UI.Controls.Navigation("chrome_ctrl_container", options);
  32:         nav.setVisible(true);
  33:     };
  34:  
  35:     // Callback for the onCssLoaded event defined
  36:     //  in the options object of the chrome control
  37:     function chromeLoaded() {
  38:         "use strict";
  39:         // When the page has loaded the required
  40:         //  resources for the chrome control,
  41:         //  display the page body.
  42:         $("body").show();
  43:     }
  44:  
  45:     return {
  46:         render: render,
  47:         chromeLoaded: chromeLoaded
  48:     };
  49: }();
  50:  
  51: SPWorkshop.Helper = function () {
  52:     var getQueryStringParameter = function (p) {
  53:         "use strict";
  54:         var params = document.URL.split("?")[1].split("&");
  55:         var strParams = "";
  56:         for (var i = 0; i < params.length; i = i + 1) {
  57:             var singleParam = params[i].split("=");
  58:             if (singleParam[0] === p)
  59:                 return singleParam[1];
  60:         }
  61:     };
  62:  
  63:     return {
  64:         getQueryStringParameter: getQueryStringParameter
  65:  
  66:     };
  67: }();
  68:  
  69: $(document).ready(function () {
  70:     "use strict";
  71:     //Get the URI decoded URL.
  72:     var hostWebUrl = decodeURIComponent(SPWorkshop.Helper.getQueryStringParameter("SPHostUrl"));
  73:  
  74:     // The SharePoint js files URL are in the form:
  75:     // web_url/_layouts/15/resource
  76:     var scriptBase = hostWebUrl + "/_layouts/15/";
  77:  
  78:     // Load the js file and continue to the success handler
  79:     $.getScript(scriptBase + "SP.UI.Controls.js", SPWorkshop.ChromeControl.render);
  80: });

So pretty much just copy-paste from the MSDN article above. The important part is the SPWorkshop.ChromeControl.render() method. Here we first define the options for the Chrome Control (lines 11-29) and then load it in the line 31. In the options I specified several other pages that should be linked up in the Chrome Control (Contacts.aspx, Welcome.aspx, Help.aspx) and I want to pass the original URL query string to all those pages so that they can access all the SharePoint context info passed in the URL. I did it like this:


"linkUrl": "../Pages/Contacts.aspx?" + document.URL.split("?")[1]


And this works fine, when I load the page, my Chrome Control is loaded and when I click on the “Settings” wheel in the top right corner I can see the link to the Contacts.aspx page in the status bar containing the query string with the SharePoint context info:


image


When I click on the Contacts link, I am redirected to the Contacts.aspx page and the entire query string is copied over, so my URL looks something like


http://localhost:59891/Pages/Contacts.aspx?SPHostUrl=http%3A%2F%2Fdev%2Econtoso%2Edev&SPLanguage=en%2DUS&SPClientTag=2&SPProductNumber=15%2E0%2E4505%2E1005&SPHostTitle=Contoso%20Developer%20Site


See that pulp after Contacts.aspx? That’s the context info that SharePoint passed to my Default.aspx and that I now passed on to the Contacts.aspx thanks to that “…document.URL.split("?")[1]” line in the Chrome Control options. So far, so good.


Now on to the problem. The alternative way to load the Chrome Control in your page is declaratively through your aspx markup, something like this:



...
<body>
    <form id="form1" runat="server">
        <div id="chrome_ctrl_container" 
            data-ms-control="SP.UI.Controls.Navigation"
            data-ms-options=
            '{ 
                "appIconUrl": "../Images/AppIcon.png", 
                "appTitle": "SharePoint 2013 App", 
                "appHelpPageUrl": "../Pages/Help.aspx?" + document.URL.split("?")[1], 
                "settingsLinks": [ 
                    { 
                        "linkUrl": "../Pages/Contacts.aspx" + document.URL.split("?")[1], 
                        "displayName": "Contacts" 
                    }, 
                    { 
                        "linkUrl": "../Pages/Welcome.aspx" + document.URL.split("?")[1], 
                        "displayName": "Welcome" 
                    } 
                ] 
            }'>
        </div>
        <div>
            <h1 class="ms-accentText">Hello World</h1>
        </div>
    </form>
</body>
...

See those “data-ms-control” and the “data-ms-options” attributes? The JavaScript code in the SP.UI.Controls.js recognizes them and renders the Chrome Control in the parent HTML element (here the “chrome_ctrl_container” div).


Now all you need to actually render the control at runtime is this piece of JavaScript in your document load handler:



$(document).ready(function () {
    "use strict";
    //Get the URI decoded URL.
    var hostWebUrl = decodeURIComponent(SPWorkshop.Helper.getQueryStringParameter("SPHostUrl"));
 
    // The SharePoint js files URL are in the form:
    // web_url/_layouts/15/resource
    var scriptBase = hostWebUrl + "/_layouts/15/";
 
    // Load the js file and continue to the success handler
    $.getScript(scriptBase + "SP.UI.Controls.debug.js");
    
});

However, when you deploy your app and browse to the page, instead of the Chrome Control you will be greeted by the nasty “Invalid JSON data".” JavaScript exception and a blank screen:


image



After some digging in the SP.UI.Controls.js I found the root cause. When declaring the Chrome Control declaratively in the aspx markup, the SP.UI.Controls.js checks if the Chrome options passed in the data-ms-options attribute are a valid JSON string according to the JSON standard from http://json.org, and in this case this is NOT a valid JSON thanks to those “ + document.URL.split("?")[1]” calls that are supposed to pass the original query string to other pages. However, in this case this string passed in the data-ms-options attribute doesn’t have to be a valid JSON string, since it is later executed using the JavaScript eval() function, so it should only be a valid JavaScript code which it is. So we have a bug here.


To prove that this is correct, remove the “+ document.URL.split("?")[1]” from the markup and redeploy your app again, this time the Chrome should load fine, except that your links to the Contacts.aspx and other pages now no longer get all the nice info from the URL query string (again, pay attention to the URL in the status bar, now without the query string):



<body>
    <form id="form1" runat="server">
        <div id="chrome_ctrl_container" 
            data-ms-control="SP.UI.Controls.Navigation"
            data-ms-options=
            '{ 
                "appIconUrl": "../Images/AppIcon.png", 
                "appTitle": "SharePoint 2013 App", 
                "appHelpPageUrl": "../Pages/Help.aspx?", 
                "settingsLinks": [ 
                    { 
                        "linkUrl": "../Pages/Contacts.aspx", 
                        "displayName": "Contacts" 
                    }, 
                    { 
                        "linkUrl": "../Pages/Welcome.aspx", 
                        "displayName": "Welcome" 
                    } 
                ] 
            }'>
        </div>
        <div>
            <h1 class="ms-accentText">Hello World</h1>
        </div>
    </form>
</body>

image


So how do you pass the query string to the pages called from the Chrome Control when it is declared declaratively in the aspx markup? You don’t, there is no way to circumvent this bug unless you change the SP.UI.Control.js which is not supported. In other words, you will have to use the JavaScript way to load the Chrome Control until this is fixed.


Note: examples similar to this one with attempts to pass the query string to the pages in a declarative Chrome Control were used in highly popular and otherwise highly recommended books “Inside Microsoft SharePoint 2013” and “Microsoft SharePoint 2013 App Development” and they won’t work either due to the same problem – so don’t waste your time looking for the solution, there is none at this moment (SP 2013 with April 2013 CU).