In October 2007, Reuben Krippner and Phil Rawlinson presented two breakout sessions at Convergence EMEA in Copenhagen:
1. Microsoft Dynamics CRM and SharePoint: Better Together
2. Microsoft Dynamics CRM Extensibility
During these sessions we covered a number of live demonstrations of how you can extend and integrate Microsoft Dynamics CRM (see other posts within our blog). One of the “big hit” demos involved the creation of contextual document libraries and folders triggered by Microsoft Dynamics CRM. I have packaged up this example as a hands-on-lab for you to try for yourself.
A few explanatory notes first! As always, the code samples are supplied as is with no support or warranty provided by Microsoft. Feel free to take this code and modify and extend as you see fit. Special thanks go to Rich Dickinson from the Microsoft Dynamics CRM product group who set out the concepts and some code snippets for me to borrow and extend! I will also be posting this example modified to work with Microsoft Dynamics CRM 4.0 soon so stay tuned.
Overall objectives:
We will create a post-create callout component that will create a contextual document library within SharePoint whenever a new Account record is created in Microsoft Dynamics CRM. SharePoint provides us with web services that allow us to programmatically create document libraries. The code sample also then writes the URL of the document library to custom attributes in the Account entity. The contextual document library is displayed in an iFrame by some custom JavaScript in the onLoad event for the Account form in Microsoft Dynamics CRM. Additionally, logic has been added to the callout so that a contextual document folder (within the document library) is created for each sales opportunity created against the account record.
This example has been tested using Microsoft Dynamics CRM 3.0 and Microsoft Office SharePoint Server (MOSS) 2007.
Steps to follow:
Step 1: We need to add some custom attributes to the Account and Opportunity entities, these attributes are used to store the location of the contextual document libraries and folders that we create via the callout component. Create the following attributes for the Account entity:
Step 2: Save the changes
Step 3: Create the following attributes for the Opportunity entity:
Step 4: Save the changes
Step 5: Publish the changes so that the CRM web services are up to date with our new attributes.
Step 6: Create a class library project in Visual Studio to create our post-callout component.
Step 7: Add web references for the following:
Step 8: Ensure you are editing the main class file in your project (I called this file doclist.cs)
using System; using System.IO; using System.Text; using System.Xml; using Microsoft.Crm.Callout;
namespace crmsp.libraries { public class doclist : CrmCalloutBase { //use this function for debugging and auditing where required private void WriteToFile(string message) { using (StreamWriter sw = File.CreateText(@"c:\audit\callout_test_output.txt")) { sw.WriteLine(message); } }
public static System.Xml.XmlNode CreateFolder(string listName, string folderName) { //Creating a SharePoint document folder under a library requires us to pass an Xml document to the web service // // The XML document looks like this: // // <Batch OnError="Return" RootFolder=""> // <Method ID="1" Cmd="New"> // <Field Name="ID">New</Field> // <Field Name="FSObjType">1</Field> // <Field Name="BaseName">foldername</Field> // </Method> // </Batch> // Let's set up an XML document and populate it with the settings for our new document folder System.IO.StringWriter sw = new System.IO.StringWriter(); System.Xml.XmlTextWriter xw = new System.Xml.XmlTextWriter(sw); xw.WriteStartDocument();
// build batch node xw.WriteStartElement("Batch"); xw.WriteAttributeString("OnError", "Return"); xw.WriteAttributeString("RootFolder", "");
// Build method node xw.WriteStartElement("Method");
// Set transaction ID xw.WriteAttributeString("ID", Guid.NewGuid().ToString("n")); xw.WriteAttributeString("Cmd", "New");
// Build field ID xw.WriteStartElement("Field"); xw.WriteAttributeString("Name", "ID"); xw.WriteString("New"); xw.WriteEndElement(); // Field end
// Build FSObjType xw.WriteStartElement("Field"); xw.WriteAttributeString("Name", "FSObjType"); xw.WriteString("1"); // 1= folder xw.WriteEndElement(); // Field end
// Build Base Name field from folder xw.WriteStartElement("Field"); xw.WriteAttributeString("Name", "BaseName"); xw.WriteString(folderName); xw.WriteEndElement(); // Field end
// Close Method & Batch elements xw.WriteEndElement(); // Method end xw.WriteEndElement(); // Batch end xw.WriteEndDocument(); System.Xml.XmlDocument batchElement = new System.Xml.XmlDocument(); batchElement.LoadXml(sw.GetStringBuilder().ToString());
//Setup web service Lists listService = new Lists(); listService.Credentials = new System.Net.NetworkCredential("username", "password", "domain name");
// send update request to sharepoint to create the document folder System.Xml.XmlNode result = listService.UpdateListItems(listName, batchElement); return result; }
public override void PostCreate(CalloutUserContext userContext, CalloutEntityContext entityContext, string postImageEntityXml) { //SharePoint Document Libraries have a type code of 101 Int32 spDocLibraryListType = 101;
//If this is a create of an account then create a document library if (entityContext.EntityTypeCode == 1) { WriteToFile(postImageEntityXml.ToString()); // Load the postImageEntityXml and use an XPath // query to locate the name field value. XmlDocument xd = new XmlDocument(); xd.LoadXml(postImageEntityXml); // Initialize a NameTable object. NameTable nt = new NameTable(); // Initialize a namespace manager. XmlNamespaceManager nsmgr = new XmlNamespaceManager(nt); // Add the required prefix/namespace pairs to the namespace // manager. Add a default namespace first. nsmgr.AddNamespace("crm","http://schemas.microsoft.com/crm/2006/WebServices"); nsmgr.AddNamespace("xsi","http://www.w3.org/2001/XMLSchema-instance"); XmlNode node = xd.SelectSingleNode("//crm:Property[@Name='name']/crm:Value", nsmgr); // Grab the node's InnerText property to name our SharePoint Document Library. if (node != null) { string accountName = node.InnerText; //string listName = node.InnerText + "_" + DateTime.Now.ToString("yyyyMMdd_hhmmss"); //Call the SharePoint Web Service to create a document library Lists listService = new Lists(); listService.Credentials = new System.Net.NetworkCredential("user name", "password", "domain name"); //System.Xml.XmlNode result = listService.AddList(listName, accountName + " Document Library", spDocLibraryListType); System.Xml.XmlNode result = listService.AddList(accountName, accountName + " Document Library", spDocLibraryListType); //grab the return xml string returnXml = result.InnerXml.ToString(); //update Microsoft CRM record with the return xml CrmService service = new CrmService(); service.Credentials = System.Net.CredentialCache.DefaultCredentials;
// Create the account object. account account = new account();
// Set the properties of the account object to be updated. account.new_sharepointdoclibraryname = accountName; account.new_sharepointdoclibraryurl = "http://yoursharepointservername/" + accountName + "/Forms/AllItems.aspx";
// accountid is a key that references the ID of the account to be updated. account.accountid = new Key();
// account.accountid.Value is the GUID of the record to be changed. account.accountid.Value = entityContext.InstanceId;
// Update the account. service.Update(account); } }
//If this is a create of an opportunity then create a folder under the account's document library if (entityContext.EntityTypeCode == 3) { WriteToFile(postImageEntityXml.ToString()); //If the Opportunity is linked to an account then we want to create a document folder // // Load the postImageEntityXml and use an XPath // query to locate the name field value. XmlDocument xd = new XmlDocument(); xd.LoadXml(postImageEntityXml); // Initialize a NameTable object. NameTable nt = new NameTable(); // Initialize a namespace manager. XmlNamespaceManager nsmgr = new XmlNamespaceManager(nt); // Add the required prefix/namespace pairs to the namespace // manager. Add a default namespace first. nsmgr.AddNamespace("crm", "http://schemas.microsoft.com/crm/2006/WebServices"); nsmgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance"); XmlNode node = xd.SelectSingleNode("//crm:Property[@Name='customerid']/crm:Value[@type='account']", nsmgr); // Grab the node's InnerText property to see if this opportunity is related to an account. if (node != null) { //this node gives us the Guid of the Account record string strAccountId = node.InnerText; //we now need to query the account to grab the SharePoint document library name CrmService service = new CrmService(); service.Credentials = System.Net.CredentialCache.DefaultCredentials;
//we'll also grab the Title of the sales Opportunity for naming our SharePoint folder XmlNode nodeTitle = xd.SelectSingleNode("//crm:Property[@Name='name']/crm:Value", nsmgr);
// Create the column set object that indicates the fields to be retrieved. ColumnSet cols = new ColumnSet(); // Set the properties of the column set. cols.Attributes = new string[] { "new_sharepointdoclibraryname" }; // accountGuid is the GUID of the record being retrieved. Guid accountGuid = new Guid(strAccountId); // Retrieve the account. // The EntityName indicates the EntityType of the object being retrieved. account account = (account)service.Retrieve(EntityName.account.ToString(), accountGuid, cols);
//OK, now we have the sharepoint document library name we can create a document folder under it //the folder name will be "Sales Opp" + the Guid of the Opportunity record //string strFolderName = "Sales Opp - " + nodeTitle.InnerText + "_" + DateTime.Now.ToString("yyyyMMdd_hhmmss"); string strFolderName = "Sales Opp - " + nodeTitle.InnerText; XmlNode xmlResult = CreateFolder(account.new_sharepointdoclibraryname, strFolderName);
//Finally, we need to update the opportunity with the folder location // Create an opportunity object. opportunity opportunity = new opportunity();
// Set the properties of the opportunity object to be updated. opportunity.new_sharepointlibraryxml = xmlResult.InnerXml.ToString(); opportunity.new_sharepointdoclibraryurl = "http://yoursharepointserver/" + account.new_sharepointdoclibraryname + "/Forms/AllItems.aspx?RootFolder=/" + account.new_sharepointdoclibraryname + "/" + strFolderName;
// opportunityid is a key that references the ID of the opportunity to be updated. opportunity.opportunityid = new Key();
// opportunity.Value is the GUID of the record to be changed. opportunity.opportunityid.Value = entityContext.InstanceId;
// Update the opportunity. service.Update(opportunity); } } } } }
Step 9: Compile your new callout component.
Step 10: Copy the dll file created into the \server\bin\assembly folder on your CRM Server(s).
Step 11: Add the Callout.Config.xml snippet into your callout.config file.
<callout.config version="1.0" xmlns=" http://schemas.microsoft.com/crm/2006/callout/"> <callout entity="account" event="PostCreate"> <subscription assembly="CRM_DOCS.dll" class="crmsp.libraries.doclist" onerror="abort"> </subscription> </callout> </callout.config>
Step 12: Run IISReset to enable the new callout event
Step 13: We now need to configure the iFrames in the Account and Sales Opportunity entities to display our contextual document libraries or folders. Starting with Account form design: add a new tab called Document Library and then add a new iFrame called IFRAME_Sharepoint
Step 14: Next we want to add in the OnLoad logic on the Account form to set the URL of the SharePoint iFrame to display our contextual document library.
// OnLoad Event to set the iFrame URL for the SharePoint Library. var CRM_FORM_TYPE_CREATE = 1; var CRM_FORM_TYPE_UPDATE = 2; //////////////////////////////////////////////////////////////////////////////// // Set SharePoint Document Library ////////////////////////////////////////////////////////////////////////////////
// read-only forms are not editable and so do not require dynamic picklists. if (crmForm.FormType == CRM_FORM_TYPE_UPDATE) { var sUrl=crmForm.all.new_sharepointdoclibraryurl.DataValue; crmForm.all.IFRAME_SharePoint.src = sUrl; }
Step 15: Save the changes to the Account Entity.
Step 16: Now, working with the Opportunity entity: add a new tab called Document Folder and then add a new iFrame called IFRAME_Sharepoint.
Step 17: We now need to add the OnLoad logic to the Opportunity form.
// OnLoad Event to set the sharepoint document library iFrame
var CRM_FORM_TYPE_CREATE = 1; var CRM_FORM_TYPE_UPDATE = 2; //////////////////////////////////////////////////////////////////////////////// // Set SharePoint Document Folder ////////////////////////////////////////////////////////////////////////////////
// we'll update the iFrame src property if this is an update form if (crmForm.FormType == CRM_FORM_TYPE_UPDATE) { var sUrl=crmForm.all.new_sharepointdoclibraryurl.DataValue; crmForm.all.IFRAME_SPDocFolder.src = sUrl; }
Step 18: Finally, save changes to the Opportunity entity and Publish all your changes. The result should be that a new document library is created whenever a new Account record is added to Microsoft Dynamics CRM and a new Document Folder for each Sales Opportunity is created when Sales Opportunities are added to that Account record.
Have fun with it! :-) I'll post another entry once I've gone through it on Microsoft Dynamics CRM 4.0
Kind regards, Reuben Krippner (UK Microsoft Partner Technology Specialist - Microsoft Dynamics CRM)