Dynamics CRM & SharePoint

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=httpss%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

https://micrm.crgdemo.com/XRMServices/2011/OrganizationData.svc/SharePointDocumentLocationSet(guid’00000000-0000-0000-0000-000000000000′)

2016-05-18T19:43:06Z

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 "//_/" 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//contact/_/”
  • “/contact/_/”

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//contact/_/” if there was no “_” 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)

 

(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.