Use SharePoint Info Across Web Applications

sharepoint and the ability to use information about web apps
Use SharePoint information across web applications. ElevatePoint can help.

Share This Post

Share on facebook
Share on linkedin
Share on twitter
Share on email


John Huschka, January 13, 2015

I recently had an issue arise which I suspect many of you have encountered:  You have data in one SharePoint web application that you would like to display in a page in another SharePoint web application.  My specific requirement was to display employee professional credentials from a central web application within each employee’s profile page in the “My Sites” web application as follows:

It seems rather straightforward, but there are several challenges:

  1. A coded server-side solution will not solve this problem in Office 365. (No farm solutions in Office 365, and partial trust “sandboxed” applications are officially deprecated.) In addition, it would preclude using popular client-side toolsets, such as AngularJS, for displaying the information.
  2. The SharePoint app model has the potential to allow cross-domain data access; however, my solution needs to work with SharePoint 2010 (for now).  In addition, I would need to configure app hosting for the farm—which seems rather “heavy” for simply displaying a bit of information.
  3. The browser’s same-origin policy prevents using the SharePoint client-side object model from accessing the other web application.

Starting with Jeroen van Lieshout’s work, I was able to implement a rather straightforward solution by which the profile page interacts with a “proxy” page on the web with the source data.

The proxy page is embedded within the profile page using a standard SharePoint page viewer web part, and the pages communicate using HTML 5’s standard web messaging.

The remainder of this post provides details of the implementation and source code is provided at the end.

The SharePoint Proxy Page

The proxy page (DataProxy.aspx) is setup within the data web.  It contains no content except that provided by SharePoint master pages:

Behind the scenes, it configures HTML messaging such that it can handle REST requests:


var messageProcessor = {
    lastMessageSource: '',
    lastMessageOrigin: '',
    receiveMessage: function (event) {
        var eventData;
        try {
            messageProcessor.lastMessageSource = event.source;
            messageProcessor.lastMessageOrigin = event.origin;
            eventData = JSON.parse(event.data);
            //  The REST method call is supplied by the caller, but we provide the site context that this page supports.
            messageProcessor.callRestAPI("https://jehcwsserver/sites/DevTest/_api/" + eventData.method);
        } catch (error) {
        }
    },

    callRestAPI: function (restUrl) {
        $.ajax(
        {
            url: restUrl, method: "GET",
            headers:
            {
                "accept": "application/json;odata=verbose",
            },
            success: messageProcessor.onSuccess,
            error: messageProcessor.onError
        });
    },

    onSuccess: function (data) {
        if (data) {
            var response = { header: "response", message: data };
            // Forward the REST response back to the caller.
            messageProcessor.lastMessageSource.postMessage(JSON.stringify(response), messageProcessor.lastMessageOrigin);
        }
    },

    onError: function (err) {
        var response = { header: "error", message: err };
        messageProcessor.lastMessageSource.postMessage(JSON.stringify(response), messageProcessor.lastMessageOrigin);
    }
};

_spBodyOnLoadFunctionNames.push("attachEventHandlers");

function attachEventHandlers() {
    if (typeof window.addEventListener !== "undefined") {
        window.addEventListener("message", messageProcessor.receiveMessage, false);
    }
}

 

In addition to including the JavaScript above, the proxy page also has to have an “AllowFraming” element added to support embedding within other pages within an iframe. (See below.)

<%@ Page language="C#" MasterPageFile="~masterurl/default.master"
      Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c"
      meta:progid="SharePoint.WebPartPage.Document"  %>
<%@ Register tagprefix="SharePoint" namespace="Microsoft.SharePoint.WebControls"
      assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages"
      Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<asp:Content ContentPlaceHolderId="PlaceHolderAdditionalPageHead" runat="server">
<SharePoint:CssRegistration Name="default" runat="server"/>
<SharePoint:ScriptLink LoadAfterUI="false" Defer="false" name="~site/Lists/DataProxy/Scripts/jquery-2.1.1.min.js" runat="server" OnDemand="false">
     </SharePoint:ScriptLink>
<SharePoint:ScriptLink LoadAfterUI="false" Defer="false" name="~site/Lists/DataProxy/Scripts/ConfigureMessagingProcessor.js" runat="server"
     OnDemand="false"> </SharePoint:ScriptLink>
<WebPartPages:AllowFraming runat="server" __WebPartId="{B8617452-48FE-40AF-BDE9-86E9ADA1CDFD}"/>
</asp:Content>

 

The SharePoint User Profile Page, Interacting with the Proxy Page

The Profile page—the page consuming the data—uses a standard SharePoint page viewer to embed the proxy page. The title of the web part, “DataProxy”, is the critical link between the functionality in the profile page and the data proxy page. (See below.) Also note that the web part has been configured to be hidden.

On load, the profile page loads JavaScript that is responsible for interacting with the proxy page. The critical function here is “getCredentials” which is responsible for defining a function to handle a return call from the data proxy, for configuring the REST call, and for sending the REST call to the data proxy:

// The title of the iFrame in which the data proxy page is located, equivalent to web part title.
var DataProxyIFrameTitle = "DataProxy";
//  The site at which the data proxy page is located.  Will be target of message.
var DataProxySite = "https://jehcwsserver/sites/DevTest";
//  The list, in the proxy site, from which data is to be retrieved.  Will be forwarded, in a REST query, to the proxy.
var CredentialsList = "AssociateCredentials";

var credentialsAJSApp = angular.module('credentialsAJSApp', ['smart-table']);

var getParameterFromQueryString = function (queryString, paramToRetrieve) {
~~~~~ Code removed for brevity.  See download ~~~~
};

credentialsAJSApp.controller('SPCredentials', function SPCredentials($scope, SharePointJSOMCredentialsService) {
    $scope.refresh = function () {
        var promise = SharePointJSOMCredentialsService.getCredentials(CredentialsList);
        promise.then(function (result) { $scope.items = SharePointJSOMCredentialsService.items();
                                         $scope.userName = SharePointJSOMCredentialsService.userName(); }, function (reason) { alert(reason); });
    };
    $scope.refresh();
});

credentialsAJSApp.service('SharePointJSOMCredentialsService', function ($q, $http) {
    var credentials = [];
    this.accountName = "";
    this.items = function () { return credentials; };
    this.userName = function () { return this.accountName; };
    //  This is the critical function that interacts with the data proxy.
    this.getCredentials = function (listTitle) {
        var deferred = $q.defer();

        //  Setup the function that will handle the return call from the data proxy.
        var messageHandler = function (event) {
            try {
                var eventData = JSON.parse(event.data);
                eventData.message.d.results.forEach(function (entry) {
                    var row = [];
                    row.Title = entry.Title;
                    credentials.push(row);
                }
                    );
                window.removeEventListener("message", messageHandler, false);
                deferred.resolve();
            } catch (error) {
                window.removeEventListener("message", messageHandler, false);
                deferred.reject('Request failed.');
            }
        };

        //  Parse value on profile page so that account name can be displayed.
        var accountName = getParameterFromQueryString(_normalizedPageUrlForSocialItem, "accountname");
        var decodedAccount = decodeURI(accountName);
        if (decodedAccount.indexOf("") > -1) { this.accountName = decodedAccount.substring(decodedAccount.indexOf("") + 1); }
        else { this.accountName = decodedAccount; }

        //  Define REST query to be sent to data proxy page.
        var message = { method: "lists/getbytitle('" + listTitle + "')/items?$select=Title,Associate/Id,Associate/Name&$expand=Associate/Id&$filter=substringof('"
                    + decodedAccount + "',Associate/Name)" };
        var messageAsJson = JSON.stringify(message);
        window.addEventListener("message", messageHandler, false);

        //  Find data proxy page IFRAME.
        var iFrames = jQuery("iframe[title*='" + DataProxyIFrameTitle + "']");

        if (iFrames.length == 0) { alert("Unable to find expected " + DataProxyIFrameTitle + " iFrame"); }
        else {
            //  Send the message.
            iFrames[0].contentWindow.postMessage(messageAsJson, DataProxySite);
            return deferred.promise;
        }
    };
});

function BootstrapAngular() {
    //  Find data proxy page IFRAME.
    var iFrames = jQuery("iframe[title*='" + DataProxyIFrameTitle + "']");

    if (iFrames.length == 0) { alert("Unable to find expected " + DataProxyIFrameTitle + " iFrame"); }
    else {
        // Before we can attach angular (and send a message to the proxy), the proxy page must be fully loaded in the iFrame.
        iFrames[0].addEventListener("readystatechange", function (event) {
            if (event.target.readyState == 'complete') {
                angular.bootstrap(document.getElementById('CredentialsDisplay'), ['credentialsAJSApp']);
            }
        })
    }
}

//  We put the angular bootstrap on SharePoint's document ready.
_spBodyOnLoadFunctionNames.push("BootstrapAngular");

 

The title of the SharePoint Page Viewer web part and the title of the ASP.Net generated iframe are critical because the profile page’s JavaScript locates the data proxy with which it interacts by title. Here, a hidden SharePoint page viewer web part with a title of “DataProxy” generates an iframe with a title of “(Hidden) DataProxy”.

The SharePoint User Profile Page, Displaying the Data

You likely noticed the use of an AngularJS application, controller, and service in the data layer above.  There is an excellent video on Channel 9 by Jeremy Thake illustrating how to use AngularJS with SharePoint, and he has also made the code for his examples available.  Therefore, I’m not going to cover the details of using AngularJS.

I do want to show, however, how the profile page has been linked to the AngularJS controller functionality. The controller above exposes two items within the AngularJS scope:

  1. $scope.items: An array containing the credentials from SharePoint
  2. $scope.userName: A single value—the user’s account name derived from values within the profile page.
credentialsAJSApp.controller('SPCredentials', function SPCredentials($scope, SharePointJSOMCredentialsService) {
    $scope.refresh = function () {
        //alert("Running refresh in controller");
        var promise = SharePointJSOMCredentialsService.getCredentials(CredentialsList);
        promise.then(function (result) {
                                         $scope.items = SharePointJSOMCredentialsService.items();
                                         $scope.userName = SharePointJSOMCredentialsService.userName(); }
                      , function (reason) { alert(reason); });
    };

    $scope.refresh();
});

 

These values are displayed within the profile page using a standard SharePoint content editor web part in which the source has been manually edited:

Unfortunately, if we use the web page’s “Edit Source”, the AngularJS connection become corrupted. Instead, we have to use SharePoint Designer or Visual Studio to manually edit the source code to connect to AngularJS:

<WebPartPages:ContentEditorWebPart runat="server" __MarkupType="xmlmarkup" WebPart="true" __WebPartId="{3EC5A832-5DB5-46FE-A75B-008D50714187}" >
<WebPart xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns="https://schemas.microsoft.com/WebPart/v2">
  <Title>Credentials</Title>
  <FrameType>None</FrameType>
  <Description>Allows authors to enter rich text content.</Description>
  <IsIncluded>true</IsIncluded>
  <PartOrder>4</PartOrder>
  <FrameState>Normal</FrameState>
  <Height />
  <Width />
  <AllowRemove>true</AllowRemove>
  <AllowZoneChange>true</AllowZoneChange>
  <AllowMinimize>true</AllowMinimize>
  <AllowConnect>true</AllowConnect>
  <AllowEdit>true</AllowEdit>
  <AllowHide>true</AllowHide>
  <IsVisible>true</IsVisible>
  <DetailLink />
  <HelpLink />
  <HelpMode>Modeless</HelpMode>
  <Dir>Default</Dir>
  <PartImageSmall />
  <MissingAssembly>Cannot import this Web Part.</MissingAssembly>
  <PartImageLarge>/_layouts/15/images/mscontl.gif</PartImageLarge>
  <IsIncludedFilter />
  <ExportControlledProperties>true</ExportControlledProperties>
  <ConnectionID>00000000-0000-0000-0000-000000000000</ConnectionID>
  <ID>g_3ec5a832_5db5_46fe_a75b_008d50714187</ID>
  <ContentLink xmlns="https://schemas.microsoft.com/WebPart/v2/ContentEditor" />
  <Content xmlns="https://schemas.microsoft.com/WebPart/v2/ContentEditor"><![CDATA[<div class="ng-scope" id="CredentialsDisplay">  
   <div class="ng-scope" ng-controller="SPCredentials">
   <h1 class="ng-binding">{{userName}}'s Credentials</h1>
   <table st-table="items">
   <tbody>
   <tr class="ng-scope" ng-repeat="row in items">
   <td>{{row.Title}}</td></tr>
   </tbody>
   </table>
</div>
</div>]]></Content>
  <PartStorage xmlns="https://schemas.microsoft.com/WebPart/v2/ContentEditor" />
</WebPart>
</WebPartPages:ContentEditorWebPart>

 

The use of “{{}}” tagging is standard AngularJS. Our ability to use it within a table using the ng-repeat attribute is provided by Smart Table, which I included within the profile page:


<asp:Content contentplaceholderid="PlaceHolderAdditionalPageHead" runat="server">
<SharePoint:ScriptLink LoadAfterUI="false" Defer="false" name="~site/Lists/DataProxyScripts/jquery-1.9.1.min.js" runat="server" OnDemand="false">
     </SharePoint:ScriptLink>
<SharePoint:ScriptLink LoadAfterUI="false" Defer="false" name="~site/Lists/DataProxyScripts/angular.js" runat="server" OnDemand="false">
     </SharePoint:ScriptLink>
<SharePoint:ScriptLink LoadAfterUI="false" Defer="false" name="~site/Lists/DataProxyScripts/smart-table.debug.js" runat="server" OnDemand="false">
     </SharePoint:ScriptLink>
<SharePoint:ScriptLink LoadAfterUI="false" Defer="false" name="~site/Lists/DataProxyScripts/credentialsAJSApp.js" runat="server" OnDemand="false">
     </SharePoint:ScriptLink>
<SPSWC:ActivityFeedLink Consolidated="false" runat="server"/>
<SPSWC:MySiteHideDiv HideRibbonRow="true" runat="server"/>
</asp:Content>

 

Summary, Getting Assistance, and Getting the Code

In this article, I’ve provided details of an easy-to-deploy cross-domain data layer for SharePoint. There are lots of people out there with great SharePoint experience, so I welcome your questions and comments.

If you would like assistance in your SharePoint implementation, we at ElevatePoint bring years of Microsoft expertise
along with a commitment to helping you be successful.  Please do not hesitate to contact us.

If you would like the full source code, in the form of two Visual Studio deployable SharePoint 2013 sandboxed solutions, see CrossDomainDataLayer on Github.com.

An important caution: The solution overwrites (“unghosts”) the existing SharePoint-standard Profile.aspx page within the site to which you deploy it. Take a backup of or copy your original page before deploying.

More To Explore

remote worker
Digital Workplace

How to Work Remotely

Working remotely is hard for some people. We have ideas to make it easier.

We can help

teams and teamwork