In this post, I’ll walk through how to use the postMessage api to enable cross-frame communication between your Client App Parts and the hosting SharePoint page, to make your SharePoint App Parts more dynamic and responsive to changes in the parent page.
Overview
In the SharePoint 2013 App Model, Client App Parts are implemented as iframes into pages hosted in App Webs or Provider Hosted web applications. Because SharePoint apps are hosted on different domains, the iframes can’t use javascript to access the parent window and get information from the hosting page. This is a good thing. You wouldn’t want a Client App Part that you installed from the marketplace to be able to see the list of customers on that List View web part that sits right next to it on the page. But it does make it very hard as a developer to give Client App Parts more dynamic page information to work with. The only contextual info a Client App Part can get is through Client App Part properties.
Normal SharePoint Web Parts have a number of ways of getting initialization data from the hosting page when they first load up:
- Web Part Properties (Tool Pane)
- Web Part Connections (Passing info from Web Part to Web Part)
- Host Page QueryString
- Reading Hidden Fields or other form elements on the page
- Built-in SharePoint objects like _spPageContextInfo
- Reading the metadata of the host page
Unfortunately, SharePoint Client App Parts can only get information from the first option, through Client App Part Properties. When you build your Client App Part with custom properties, they appear in the App Part’s tool pane when you edit the App Part, similar to normal Web Part toolpane properties. Client App Part properties are a little different though – setting these actually appends a querystring parameter to the App Part’s iframe src
attribute, which your App Part page can then read when it starts up. That’s good for setting a one-time configuration of your App Part, but you can’t change any of those properties dynamically in response to changes in the host page.
In this post, I’ll show you how to pass additional data from the host page to your Client App Parts via the postMessage api, effectively opening the door to leveraging 2-6 above. The example I’ll show will be an App Part placed on the MySite Host’s About Me page. The App Part will read the host page’s accountname querystring parameter to determine which user profile is being viewed, and will display some basic profile information about that user.
The postMessage API
The postMessage API was designed to enable safe, cross-domain communication between windows or iframes. Essentially each window needs to opt-in to accepting communications from other cross-origin/cross-domain windows. Any window or iframe can send messages to another window or iframe just by calling the postMessage method. If the target window hasn’t opted-in to listen for these messages, the messages are just silently ignored.
Requesting Information from the Host Page
The first step in getting this to work is to configure your Client App Part page to send a request to the host page, and to configure a listener for the host page’s response.
To start this out, using Visual Studio 2013, I created a new SharePoint App project called PostMessageApp, added a Client Web Part (Host Web) called PostMessageAppPart, and created a new page for the App Part called PostMessageAppPartPage.aspx. I gave the App Part the Read permissions to the User Profile (Social) permission set.
In the App Part page, I added some basic HTML markup:
1 2 3 4 5 6 7 8 9 |
<body> <div> Host page's full url:<br /> <textarea id="hostpageurl" readonly="readonly" style="width:100%;height:75px;overflow:scroll;" ></textarea><br /><br /> First name: <span id="firstname"></span><br /> Last name: <span id="lastname"></span><br /> Department: <span id="department"></span><br /> </div> </body> |
Here is what the app part looks like empty:
In the newly created App Part page, I added a javascript function to the page head called getQueryStringParameter which is needed to extract querystring information. You’ve probably seen a similar function in many of the SharePoint Hosted App code samples on the internet. This version below includes an overload to get a querystring value from a passed-in url (rather than the current document’s url), which will be used later:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function getQueryStringParameter(key, urlToParse) { /// <signature> /// <summary>Gets a querystring parameter, case sensitive.</summary> /// <param name="key" type="String">The querystring key (case sensitive).</param> /// <param name="urlToParse" type="String">A url to parse.</param> /// </signature> /// <signature> /// <summary>Gets a querystring parameter from the document's URL, case sensitive.</summary> /// <param name="key" type="String">The querystring key (case sensitive).</param> /// </signature> if (!urlToParse || urlToParse.length === 0) { urlToParse = document.URL; } if (urlToParse.indexOf("?") === -1) { return ""; } var params = urlToParse.split('?')[1].split('&'); for (var i = 0; i < params.length; i = i + 1) { var singleParam = params[i].split('='); if (singleParam[0] === key) { return decodeURIComponent(singleParam[1]); } } return ""; } |
After including the querystring function, I added the following script underneath it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
(function () { var getHostPageInfoListener = function (e) { /// <summary>Callback function for getting the host page's info via postMessage api.</summary> console.log("postMessage response received: " + e.data); var messageData; try { messageData = JSON.parse(e.data); } catch (error) { console.log("Unable to parse the response from the host page."); return; } } // Register the listener if (typeof window.addEventListener !== 'undefined') { window.addEventListener('message', getHostPageInfoListener, false); } else if (typeof window.attachEvent !== 'undefined') { window.attachEvent('onmessage', getHostPageInfoListener); } // Send the host page a message var hostPageMessage = {}; hostPageMessage.message = "getHostPageInfo"; var hostPageMessageString = JSON.stringify(hostPageMessage); window.parent.postMessage(hostPageMessageString, document.referrer); console.log("Sent host page a message: " + hostPageMessageString); })(); |
This script accomplishes two things: 1) it sends a message to the host page requesting information, and 2) it registers a listener (callback) for the response from the host page. Notice that I’m sending a stringified JSON object to the host page, which I will reconstitute later. When you send messages between windows, you are just sending one string of data. You can choose to send XML, plain text, or even stringified JSON.
At this point I can send a message to the host page, but since the host page isn’t listening for it yet, the message will just silently get ignored.
Sending Information Back to the Client App Part Page
The next step in getting this to work is to configure the host page to listen and respond to the request for more information. For this, some javascript must be placed on the host page to opt-in to receiving messages from other windows. You can add javascript to a SharePoint page in a variety of ways, through a Content Editor or Script Editor web part, by editing the page or page layout in SharePoint Designer, or by adding the script directly to a masterpage.
For this example, I am going to edit the About Me page in a MySite Host site collection (“Person.aspx”) with SharePoint Designer to add the following script. I am going to add the script to the PlaceHolderAdditionalPageHead area:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<asp:Content contentplaceholderid="PlaceHolderAdditionalPageHead" runat="server"> <SPSWC:ActivityFeedLink Consolidated="false" runat="server"/> <SPSWC:MySiteHideDiv HideRibbonRow="true" runat="server"/> <script type="text/javascript"> (function() { var sendHostPageInfoListener = function (e) { var messageData; try { messageData = JSON.parse(e.data); } catch (error) { console.log("Could not parse the message response."); return; } // Construct the return data to send to the app part var returnData = {}; returnData._spPageContextInfo = _spPageContextInfo; returnData.hostPageURL = document.URL; var returnDataString = JSON.stringify(returnData); e.source.postMessage(returnDataString, e.origin); console.log("Sent app part iframe message: " + returnDataString); }; // Register the listener if (typeof window.addEventListener !== 'undefined') { window.addEventListener('message', sendHostPageInfoListener, false); } else if (typeof window.attachEvent !== 'undefined') { window.attachEvent('onmessage', sendHostPageInfoListener); } })(); </script> </asp:Content> |
The code above does two things: 1) Opts-in to listen for messages from other windows, and 2) Sends JSON data about the current page to the window that sent the request. I’ve chosen here to send a couple of pieces of information. First, I sent the full url of the host page, including any querystring parameters, by passing the document.URL. Second, I’ve also chosen to send the _spPageContextInfo object of the host page, which contains some rich data about the host page. This is just to demonstrate that you aren’t limited to sending plain text, you can serialize javascript objects from the host page as well.
With the App Part and the script above on the page, I can now see the communication back and forth between the windows in the console, and can view the returned data in a watch window. Notice the host page’s _spPageContextInfo is fully deserialized and expanded into a javascript object for use in the App Part.
Securing the Requests
By putting these scripts in place, I have allowed my host page to give information to any window or iframe that sends a request message. It’s a good idea to make this more secure, and only give host page information out to windows that you trust. You can get as restrictive or permissive as you want/need with this validation.
For home-grown side-loaded SharePoint apps, this could be as simple as passing a shared key around, and checking on both sides. To demonstrate this basic validation, I’ll add a couple of lines to the code in the Client App Part page to store and send the secret key and to parse and read it from the host page:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
" ]var secretKey = "mysecretkeyxyz"; (function () { var getHostPageInfoListener = function (e) { /// <summary>Callback function for getting the host page's info via postMessage api.</summary> console.log("postMessage response received: " + e.data); var messageData; try { messageData = JSON.parse(e.data); } catch (error) { console.log("Unable to parse the response from the host page."); return; } if (!messageData || !messageData.secretKey || messageData.secretKey !== secretKey) { console.log("Could not validate the message."); return; } }; // Register the listener if (typeof window.addEventListener !== 'undefined') { window.addEventListener('message', getHostPageInfoListener, false); } else if (typeof window.attachEvent !== 'undefined') { window.attachEvent('onmessage', getHostPageInfoListener); } // Send the host page a message var hostPageMessage = {}; hostPageMessage.secretKey = secretKey; hostPageMessage.message = "getHostPageInfo"; var hostPageMessageString = JSON.stringify(hostPageMessage); window.parent.postMessage(hostPageMessageString, document.referrer); console.log("Sent host page a message: " + hostPageMessageString); })(); |
On the host page, I’ve also added some validation code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
" ](function() { var sendHostPageInfoListener = function (e) { var secretkey = "mysecretkeyxyz"; var messageData; try { messageData = JSON.parse(e.data); } catch (error) { console.log("Could not parse the message response."); return; } if (!messageData || !messageData.secretKey || messageData.secretKey !== secretKey) { console.log("Could not validate message."); return; } // Validate that it contains the expected instruction if (messageData.message !== "getHostPageInfo") { return; } // Construct the return data to send to the app part var returnData = {}; returnData.secretKey = secretKey; returnData._spPageContextInfo = _spPageContextInfo; returnData.hostPageUrl = document.URL; var returnDataString = JSON.stringify(returnData); e.source.postMessage(returnDataString, e.origin); console.log("Sent app part iframe message: " + returnDataString); }; // Register the listener if (typeof window.addEventListener !== 'undefined') { window.addEventListener('message', sendHostPageInfoListener, false); } else if (typeof window.attachEvent !== 'undefined') { window.attachEvent('onmessage', sendHostPageInfoListener); } })(); |
For the purposes of this example, this basic validation will suffice to demonstrate the point, but I would recommend beefing this up in your own implementations.
Now, on to the last part, consuming the information.
Consuming the Data
Once you’ve got these hooked up, you can work with the data. In the App Part Page, I modified the getHostPageInfoListener function with some code to get the accountname querystring parameter from the data the host page returned, and to query the User Profile REST api to get data about that user:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
;" ] var getHostPageInfoListener = function (e) { /// <summary>Callback function for getting the host page's info via postMessage api.</summary> console.log("postMessage response received: " + e.data); var messageData; try { messageData = JSON.parse(e.data); } catch (error) { console.log("Unable to parse the response from the host page."); return; } if (!messageData || !messageData.secretKey || messageData.secretKey !== secretKey) { console.log("Could not validate the message."); return; } var appWebUrl = getQueryStringParameter("SPAppWebUrl"); var requestUrl = ""; var requestData = {}; var accountName = getQueryStringParameter("accountname", messageData.hostPageURL); if (!accountName || accountName.length === 0) { console.log("Could not find an accountname querystring parameter, using the current user."); // Use the current user instead. requestUrl = appWebUrl + "/_api/SP.UserProfiles.PeopleManager/GetMyProperties"; } else { requestUrl = appWebUrl + "/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)"; requestData["@v"] = "'" + accountName + "'"; } jQuery("#hostpageurl").html(messageData.hostPageURL); jQuery.ajax({ url: requestUrl, type: "GET", data: requestData, headers:{ Accept:"application/json;odata=verbose" }, success: function (data) { var properties = data.d.UserProfileProperties.results; for (var i = 0; i < properties.length; i++) { if (properties[i].Key === "FirstName") { jQuery("#firstname").html(properties[i].Value); } else if (properties[i].Key === "LastName") { jQuery("#lastname").html(properties[i].Value); } else if (properties[i].Key === "Department") { jQuery("#department").html(properties[i].Value); } } }, error: function (jqxr, errorCode, errorThrown) { console.log(jqxr.responseText); } }); }; |
You can see the result below, where I am logged in as Adam Toth, but am viewing Harry Tuttle’s profile:
Conclusion
With a simple javascript shim in place on a host page, I’ve shown how much richer contextual data can be passed to a Client App Part. Until Microsoft implements an alternative method, the postMessage api remains a safe and viable option for tackling scenarios that sandbox or full-trust web parts have been able to do for some time.
Hi,
Sorry for my english (I’am french).
I want to do what you explain (from mysite Person.aspx). but when i want to query the User Profile REST api, I have an unauthorized message…
I don’t understand, because, i set user profils (social) to fullcontrol.
My app work fine, but not on MySite ?
May you help me ?
Thank in advance.
To call the User Profile api, my app also required to request the Tenant permission.
I set 3 premission, Web, Tenant, and Uers Profils to FullControl, but always :
Error: Access Denied. You are not authorized to perform this action or access this resource.
After changing requested permissions, I would recommend uninstalling the app, removing the app’s identity (from Site Settings > Site app permissions). Sometimes when developing, using F5 deployment doesn’t refresh the permission set.
Thanks for your help !!
But, still nothing…
I created an other simple app on a “normal” site (not my site) from VS2013 with the API, and always this error.
My code is very simple (i don’t speak about your wonderfull postMessage which work fine…) :
$(document).ready(function () {
getUserProperties();
});
var userProfileProperties;
function getUserProperties() {
var clientContext = new SP.ClientContext.get_current();
var peopleManager = new SP.UserProfiles.PeopleManager(clientContext);
userProfileProperties = peopleManager.getMyProperties();
clientContext.load(userProfileProperties);
clientContext.executeQueryAsync(onRequestSuccess, onRequestFail);
}
On sharepoint On-Premise i’m going to onRequestSuccess but on sherpoint Online… onRequestFail with the Access Denied error.
There is other things to do for sharepoint online publishing ?
Sorry, It work fine on Sharepoint on-premise but not Sharepoint online ?
Hi,
I finally found a few days ago.
In my case, Taxonomy permission was also required…
Thanks
Hi,
It is very very important and useful information with detailed explanation. It works like a charm.. and saved lot of my time. And a small suggestion, instead of using script editor web part, we can run same code in master page. So that it is available for entire site. However, based on your requirement, we can place where ever possible.
Pingback: SharePoint app Pass parameters to a Client Web Part | Share your knowledge
Just be aware that the security example given is in no way secure (the “secret” key is actually within the page).
Yes, if you wanted security you would need to do something, but it wouldn’t look anything like what is there.
Hi,
I am currently using this to pass the query string of host web URL to client web part. Everything works fine, but page within in iframe loads first and then the script executes and i am not able to get the query string value.
Is any workaround to get the query string of host web before the iframe page loads?
thanks
Chandrasekaran