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.
The basic solution structure.
In the
App Part page, I added some basic HTML markup:
<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:
The App Part added to the right web part zone on Person.aspx.
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:
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:
(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:
<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:
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:
(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:
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:
App Part displaying another user’s
profile information.
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.
I have copied the data from
http://www.lifeonplanetgroove.com/use-postmessage-shim-give-sharepoint-client-app-parts-information/#!prettyPhoto/-1/
Suggestion: We can run the code in master page instead of source editor web part. So that, we can consume the same feature from any where.
No comments:
Post a Comment