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:

    1. New_sharepointdoclibraryname: nvarchar
    2. New_sharepointdoclibraryurl: nvarchar

attribute1

attribute2 

Step 2: Save the changes

Step 3: Create the following attributes for the Opportunity entity:

    1. New_sharepointdoclibraryurl: nvarchar
    2. New_sharepointlibraryxml: nvarchar

attribute3

attribute4

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:

    1. Sharepoint, typically this is lists.asmx and there may be a number of these web service endpoints due to multiple SharePoint sites on your designated server. SharePoint is designed so that the web services are virtualized at every site level. All of the web services binaries are located in the <site>/_vti_bin directory. For example: http://<sharepointservername>/_vti_bin/lists.asmx
    2. Microsoft Dynamics CRM (CRM Service) which is typically found at http://<yourcrmservername>/mscrmservices/2006/crmservice.asmx.

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

account_iframe

account_iframe2

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.

account_onload_script

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

opportunity_iframe

opportunity_iframe2

Step 17: We now need to add the OnLoad logic to the Opportunity form.

opportunity_onload_script

// 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)