How to Embed SharePoint Documents Page Inside Dynamics CRM Forms

Share On Facebook
Share On Twitter
Share On Google Plus
Share On Linkedin

Data transferring

Dynamics CRM Developer Series:

Attention CRM/.NET Developers: This post will show you how to embed a SharePoint documents page directly inside CRM forms.

The Situation

When the Dynamics CRM and SharePoint integration is configured, documents can be stored in SharePoint but related to (and managed from) CRM records.

For example, let’s say your Organization tracks scanned purchase orders, or tracks C.V.s, or some other form of document related to specific records in CRM. The Dynamics CRM and SharePoint integration is a great way to leverage both platforms’ strength: SharePoint is great at managing documents, and CRM is great at relating entities together.

The Problem

However, sometimes it is desirable to have an “at a glance” view of documents associated with a record. In order to view the documents in SharePoint that are related to a record, the user must navigate to a related entities area outside of the form. (Similar to Connection, or other related entities)

For most related entities, a sub-grid can be used in the form to display the relationships immediately on the form, but there is no simple workaround for the Documents.

The Solution

If you navigate to the Documents area on a record with Document Management enabled, a quick inspection of the DOM will reveal that it is an iframe pointing to a SharePoint resource.

Fortunately, the URL of this resource is predictable and easy to reconstruct. It follows the format:

https:///crmgrid/crmgridpage.aspx?langId=&locationURL=<encodeURIComponent(folderURL)>&pageSize=250

For example:

SharePoint URL https://sharepoint.company.com/sites/crmDocs
Absolute Folder URL https://sharepoint.company.com/sites/crmDocs/contact/John Smith/
contract/Service Agreement_0000000000000000000000000000000
Language Code en-US
CRMGrid URL https://sharepoint.company.com/sites/crmDocs/crmgrid/crmgridpage.aspx?langId=en-
US&locationURL=https%3A%2F%2Fsharepoint.company.com%2Fsites%2FcrmDocs
%2Fcontact%2FJohn%20Smith%2Fcontract%2FService
%20Agreement_0000000000000000000000000000000&pageSize=250

Conveniently, Dynamics CRM stores the information about which folders documents are stored in within entities called SharePointDocumentLocations and SharePointSites. These entities can be queried from JavaScript in a web resource through the OrganizationData.svc endpoint.

The ID of the current record can be found with the Xrm.Page.data.entity.getId method.
var id = Xrm.Page.data.entity.getId();

An XMLHttpRequest can be sent from JavaScript to query the service endpoint like so:

var req = new XMLHttpRequest();
var uri = encodeURI(Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc/SharePointDocumentLocationSet?$select=SharePointDocumentLocationId&$filter=RegardingObjectId/Id eq guid'" + id + "'");
req.open("GET", uri, true);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.onreadystatechange = function () {
  if (this.readyState === 4) {
    req.onreadystatechange = null;
    if (this.status === 200) {
      var returned = JSON.parse(req.responseText).d;
      var result = returned.results[0];
      var SharePointDocumentLocationId = result.SharePointDocumentLocationId;
    }
  }
};
req.send();

This will get the default SharePointDocumentLocation entity ID for the current record. Note that this is an asynchronous request so you cannot reliably return the results, instead you will need to set up a callback or some other method of handling the response.

Deceptively, the SharePointDocumentLocation has an attribute called “AbsoluteURL”. For example:
https://crm.company.com/XRMServices/2011/OrganizationData.svc/SharePointDocumentLocationSet(guid'00000000-0000-0000-0000-000000000000')?$select=AbsoluteURL

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<entry xml:base="https://micrm.crgdemo.com/XRMServices/2011/OrganizationData.svc/" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
  <id>https://micrm.crgdemo.com/XRMServices/2011/OrganizationData.svc/SharePointDocumentLocationSet(guid'00000000-0000-0000-0000-000000000000')</id>
  <title type="text">Documents on SP2010 crgdemo 1</title>
  <updated>2016-05-18T19:43:06Z</updated>
  <author>
    <name />
  </author>
  <link rel="edit" title="SharePointDocumentLocation" href="SharePointDocumentLocationSet(guid'019e3a32-6c1c-e611-80c6-000c29bfdba3')" />
  <category term="Microsoft.Crm.Sdk.Data.Services.SharePointDocumentLocation" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
  <content type="application/xml">
    <m:properties>
      <d:AbsoluteURL m:null="true" />
    </m:properties>
  </content>
</entry>

Tauntingly, however, it is NULL. Thankfully, there is a way around this problem. Each SharePointDocumentLocation has a RelativeUrl and a ParentSiteOrLocation. The “RelativeUrl” fragment will be of the form “Service Agreement_0000000000000000000000000000000” and the “ParentSiteOrLocation” value will be an entity reference to either another SharePointDocumentLocation, or the host SharePointSite record. Therefore, we can recursively query our way up the chain of SharePointDocumentLocations until we have constructed the absolute URL of the folder for this record.

At this point, you may be wondering: If I already know the URL of my SharePoint site. Why not just assume that the record will be in a location of the format "/<schemaname>/<name>_<guid>/" and be done with it?
The answer is this: Depending on how the Document Management settings are configured, the folder tree might be anything like:

  • “/account/<accountName>_<accountGuid>/contact/<contactName>_<contactGuid>/”
  • “/account/<accountName>/contact/<contactName>_<contactGuid>/”
  • “/contact/<contactName>_<contactGuid>/”

For example, if the Contact documents are configured to be stored under Account folders, then when you first navigate to a Contact document location it will prompt you to automatically create “/account/<accountName>/contact/<contactName>_<contactGuid>/” if there was no “<accountName>_<accountGuid>” folder already created.

These potential inconsistencies mean we should rely on the OrganizationData.svc service instead of making assumptions about the folder structure. Even if you know the folder structure for certain, each entity you want to embed the Documents interface on would require a customized web resource with the hard coded path.

To complicate matters, because we have to crawl the SharePointDocumentLocation tree recursively, we have to make repeated asynchronous XMLHttpRequests. This means we cannot rely on a simple recursive function that returns the relative URL because asynchronous functions require callbacks, not return statements.

The solution we came up with, is to create the following method:

var _relativeFolderURL = '';
function BuildFolderUrlRecursively(locationID) {
  var req = new XMLHttpRequest();
  var uri = encodeURI(Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc/SharePointDocumentLocationSet(guid'" + locationID + "')?$select=RelativeUrl,ParentSiteOrLocation");
  req.open("GET", uri, true);
  req.setRequestHeader("Accept", "application/json");
  req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
  req.onreadystatechange = function () {
    if (this.readyState === 4) {
      req.onreadystatechange = null;
      if (this.status === 200) {
        var returned = JSON.parse(req.responseText).d;
        var ParentSiteOrLocation = returned.ParentSiteOrLocation;
        _relativeFolderURL = returned.RelativeUrl + '/' + ._relativeFolderURL;
        if (ParentSiteOrLocation.LogicalName === 'sharepointdocumentlocation') {
          BuildFolderUrlRecursively(ParentSiteOrLocation.Id);
        } else if (ParentSiteOrLocation.LogicalName === 'sharepointsite') {
          GetSharePointSiteURL(returned.ParentSiteOrLocation.Id);
        }
      }                    
    }
  };
  req.send();
}

This will continuously request its way up the chain, building out the _relativeFolderUrl variable as:

  1. Service Agreement_0000000000000000000000000000000/
  2. contract/Service Agreement_0000000000000000000000000000000/
  3. John Smith/contract/Service Agreement_0000000000000000000000000000000
  4. contact/John Smith/contract/Service Agreement_0000000000000000000000000000000

Then it hits a SharePointSite reference instead of a SharePointDocumentLocation reference, and requests the site’s absolute URL with the following function:

var _siteURL = '';
function GetSharePointSiteURL(siteID) {
  var req = new XMLHttpRequest();
  var uri = encodeURI(Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc/SharePointSiteSet(guid'" + siteID + "')?$select=AbsoluteURL");
  req.open("GET", uri, true);
  req.setRequestHeader("Accept", "application/json");
  req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
  req.onreadystatechange = function () {
    if (this.readyState === 4) {
      req.onreadystatechange = null;
      if (this.status === 200) {
        var returned = JSON.parse(req.responseText).d;
        _siteURL = returned.AbsoluteURL;
        initDocumentIFrame();
      }
    }
  };
  req.send();
}

Now that we have the URL of the site, and the relative URL of the folder, we can set the ‘src’ of an iFrame to show the same view within the form as you see in the Documents section. This is accomplished the following function:
function initDocumentIFrame() {
  _siteURL = _siteURL + (siteURL.endsWith('/') ? '' : '/');
  var lcidMap = { 1033: "en-US", 1036: "fr-FR", 4105: "en-CA" }
  var lang = (lcidMap[Xrm.Page.context.getUserLcid()] ? lcidMap[Xrm.Page.context.getUserLcid()] : "en-US");
  var absoluteFolderURL = o._siteURL + o._relativeFolderURL;
  var webPartUrl = o._siteURL + "crmgrid/crmgridpage.aspx?langId=" + lang + "&locationUrl=" + encodeURIComponent(absoluteFolderURL) + "&pageSize=250";
  document.getElementById(o._iframeID).src = webPartUrl;
}

To make this solution robust and compact, all the components were packed into a single HTML web resource: (DocumentsIFrame.html)
<HTML xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title></title>
    <meta charset=utf-8>
  </head>
  <body style="BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; BACKGROUND-COLOR: #ffffff; MARGIN: 0px; FONT-FAMILY: Segoe UI; FONT-SIZE: 11px; BORDER-TOP: 0px; BORDER-RIGHT: 0px">
    <script type="text/javascript">
// Initializes the iFrame with specified ID. Will print an error message in an element with the specified errorID otherwise
function InitDocumentIFrame(iframeId, errorId) {
    // the init object. Just makes storing the variables easier than passing them around in each function call
    var init = {
        _iframeID: iframeId,
    _errorID: errorId,
        _relativeFolderURL: '',
        _siteURL: '',
        Print: function (msg) {
            var debug = true;
            try {
                if (debug) { console.log(msg); }
            } catch (e) { }
        },
        run: function () { // the main function to call. It will subsequently call the other ones as each XMLHttpRequest returns
      var o = this;
            var Xrm = parent.Xrm;
            if (!Xrm) { Print('No XRM'); return; }
      // Find the end SharePointDocumentLocation for the current record, then recursively work our way up the location tree to build to absolute url.
            var id = Xrm.Page.data.entity.getId();
      if (!id) { o.initDocumentIFrame(); return; }
            var req = new XMLHttpRequest();
            var uri = encodeURI(Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc/SharePointDocumentLocationSet?$select=SharePointDocumentLocationId&$filter=RegardingObjectId/Id eq guid'" + id + "'");
            o.Print("GET " + uri);
            req.open("GET", uri, true);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.onreadystatechange = function () {
                if (this.readyState === 4) {
                    req.onreadystatechange = null;
                    if (this.status === 200) {
                        var returned = JSON.parse(req.responseText).d;
                        var result = returned.results[0];
            if (!result || !result.SharePointDocumentLocationId) { o.initDocumentIFrame(); }
                        var SharePointDocumentLocationId = result.SharePointDocumentLocationId;
                        o.Print("Base SharePointDocumentLocationId: " + SharePointDocumentLocationId);
                        o.BuildFolderUrlRecursively(SharePointDocumentLocationId);
                    }
                    else {
                        //alert(this.statusText);
                    }
                }
            };
            req.send();
        },
        BuildFolderUrlRecursively: function (locationID) {
            var o = this;
            var Xrm = parent.Xrm;
            if (!Xrm) { Print('No XRM'); return; }
      // Find the SharePointDocumentLocation record with the ID passed. Keep doing this recursively.
            var req = new XMLHttpRequest();
            var uri = encodeURI(Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc/SharePointDocumentLocationSet(guid'" + locationID + "')?$select=RelativeUrl,ParentSiteOrLocation");
            o.Print("GET " + uri);
            req.open("GET", uri, true);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.onreadystatechange = function () {
                if (this.readyState === 4) {
                    req.onreadystatechange = null;
                    if (this.status === 200) {
                        var returned = JSON.parse(req.responseText).d;
                        var ParentSiteOrLocation = returned.ParentSiteOrLocation;
            // build out the _relativeFolderURL
                        o._relativeFolderURL = returned.RelativeUrl + '/' + o._relativeFolderURL;
                        o.Print("ParentSiteOrLocation.Id: " + ParentSiteOrLocation.Id);
                        o.Print("ParentSiteOrLocation.LogicalName: " + ParentSiteOrLocation.LogicalName);
                        o.Print("relativeUrl: " + o._relativeFolderURL);
                        if (ParentSiteOrLocation.LogicalName === 'sharepointdocumentlocation') {
              // continue to the parent node
                            o.BuildFolderUrlRecursively(ParentSiteOrLocation.Id);
                        } else if (ParentSiteOrLocation.LogicalName === 'sharepointsite') {
              // if the parent is a SharePointSite instead of a SharePointDocumentLocation then get the site url from it
                            o.GetSharePointSiteURL(returned.ParentSiteOrLocation.Id);
                        }
                    }
                    else {
                        //alert(this.statusText);
                    }
                }
            };
            req.send();
        },
        GetSharePointSiteURL: function (siteID) {
            var o = this;
            var Xrm = parent.Xrm;
            if (!Xrm) { Print('No XRM'); return; }
      // Get the SharePointSite record that contains the folders. We can get the url from this record.
            var req = new XMLHttpRequest();
            var uri = encodeURI(Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc/SharePointSiteSet(guid'" + siteID + "')?$select=AbsoluteURL");
            req.open("GET", uri, true);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.onreadystatechange = function () {
                if (this.readyState === 4) {
                    req.onreadystatechange = null;
                    if (this.status === 200) {
                        var returned = JSON.parse(req.responseText).d;
                        o._siteURL = returned.AbsoluteURL;
                        o.Print("AbsoluteURL: " + o._siteURL);
                        o.initDocumentIFrame();
                    }
                    else {
                        alert(this.statusText);
                    }
                }
            };
            req.send();
        },
        initDocumentIFrame: function () {
            var o = this;
      o._siteURL = o._siteURL + (o._siteURL.endsWith('/') ? '' : '/');
            o.Print("IFrame ID: " + o._iframeID);
            o.Print("folderUrl: " + o._relativeFolderURL);
            o.Print("spSiteUrl: " + o._siteURL);
      // Prints the variables captured so far. The iFrame will be constructed based on these
            var Xrm = parent.Xrm;
            if (!Xrm) { Print('No XRM'); return; }
      // If there is no result, print an error
            if (!o._relativeFolderURL || !o._siteURL) {
                document.getElementById(o._errorID).innerHTML = '<div style="border: 1px dashed red; padding: 50px; margin: 10px; text-align: center;">There is no SharePoint Document Location for this record yet. It either may not have a location created yet, or this may be a new record. Save the record then navigate to the Documents area to create one.</div>';
        document.getElementById(o._iframeID).style = "display:none;";
                return;
            }
      // Convert the lcid to the language code, ie 1033 -> "en-US" for SharePoint.
      // Extra language packs may be required on SharePoint to support extra language options
            var lcidMap = { 1033: "en-US", 1036: "fr-FR", 4105: "en-CA" }
            var lang = (lcidMap[Xrm.Page.context.getUserLcid()] ? lcidMap[Xrm.Page.context.getUserLcid()] : "en-US");
      var absoluteFolderURL = o._siteURL + o._relativeFolderURL;
      o.Print("Folder URL: " + absoluteFolderURL);
      // Construct the url of the webpart view for this record's documents
            var webPartUrl = o._siteURL + "crmgrid/crmgridpage.aspx?langId=" + lang + "&locationUrl=" + encodeURIComponent(absoluteFolderURL) + "&pageSize=100";
            o.Print("crmgrid WebPart URL: " + webPartUrl);
            document.getElementById(o._iframeID).src = webPartUrl;
        }
    };
    init.run();
}
    </script>
    <iframe id="documentIframe" style="border: none; min-width: 250px; min-height: 340px; width: 95%; margin: 5px; padding: 5px;"></iframe>
    <div id="errorMessagePane"></div>
    <script type="text/javascript">
      InitDocumentIFrame('documentIframe', 'errorMessagePane');
    </script>
  </body>
</html>

(Note: Download the full code here)

This web resource can then be added to forms which have document management enabled, without requiring any functional changes.

For additional convenience, an unmanaged solution for Dynamics CRM 2016 containing this web resource is available for download here.


We can help! If you have any questions or would like assistance configuring your Dynamics System for automated exchange rate updates, please contact the experts at CRGroup at 1.800.576.6215 or email us at crg@crgroup.com


 

mattfoy2About the Author:

Matthew Foy is a .NET developer working on CRGroup’s SharePoint and CRM consultancy team. He has a passion for robust solutions and a keen interest in solving problems.

 

 

Show Buttons
Hide Buttons