Friday, November 20, 2015

Uploading Files directly to SharePoint and link in CRM

Ran into a problem recently where a client wants a web form to interact with CRM, allow the end user to upload a file directly to SharePoint online, then have it show in the associated documents on the CRM record.

I promptly went to my favorite search engine and began to research this problem...but after many hours and searches I came up with nada, zip, zilch.  A lot of stuff about SharePoint, and even some stuff about interacting with SharePoint from within a CRM plugin, but nothing related to the use case I was working on.

So after much research, time, and general reverse engineering of how CRM shows related documents in SharePoint, I came up with something.

The way that the SharePoint integration actually works when you click the 'Documents' link on a record is that it simply shows you the files in that document library and folder. CRM only stores a 'location' record for that regardingobjectid in an entity called SharePointDocumentLocation.  That record has a parent reference in the same entity.

Once you establish the location reference, you can go directly to that folder in SP, add a new file, go back to CRM and you will see the new file in the documents view.

First, let me explain that, as far as I know, this scenario only works with CRM Online and SharePoint Online in the same Office 365 tenant.  If you have some other combination of Online and On-Premise, or all On-Premise I have no idea if this will work.  Feel free to give it a try, but I don't offer any warranty expressed or implied.

CRM and SharePoint Prep Work:

  1. Make sure you have already setup the SharePoint integration in CRM.  There is plenty of documentation for this so I won't bother to describe it here.
  2. Make sure that your OOTB or Custom Entity is setup for document management.  Again, there is plenty of documentation on this.
  3. As part of step 2, you 'should' end up with a document library in SharePoint with the same name as your entity.  

Code Notes:  
  1. I have this code setup as a console app, but you should be able to convert it to use in a web page as necessary.  It won't work with an CRM Online Plugin because it requires some SharePoint .NET libraries that you can't get access to, or even ILMerge.  There are some docs on that process and using the SharePoint REST endpoint, but I didn't use that method.
  2. .NET Framework is 4.5.2 for this code
  3. You will have to get the following nuget packages for this to work
    • "Microsoft.CrmSdk.CoreAssemblies" version="7.1.0" 
    • "Microsoft.CrmSdk.Deployment" version="7.1.0" 
    • "Microsoft.CrmSdk.Extensions" version="7.1.0" 
    • "Microsoft.IdentityModel" version="6.1.7600.16394" 
    • "Microsoft.SharePointOnline.CSOM" version="16.1.3912.1204" 
  4. You also have to have generated your XRM Class file using the CRMSVCUTIL tool.

=================

using Microsoft.SharePoint.Client;
using System;
using System.Linq;
using System.Security;
using XRMClass;

namespace zSPTest1
{
    class Program
    {
        static void Main()
        {

            string SharePointCRMDocumentsSiteName = "https://contoso.sharepoint.com/sites/crmdocs";
            string SharePointCRMDocumentsLoginName = "user.name@contoso.onmicrosoft.com";
            string SharePointCRMDocumentsPwd = "ThisIsYourPassword!";
            string SharePointCRMSchedulingRequestSiteName = "Account";
            string fileName = "attachment.pdf";
            byte[] fileContent = System.IO.File.ReadAllBytes("C:\\temp\\attachment.pdf");

            // Note, this would normally be the id of the CRM Record to relate the document to
            Guid recordId = new Guid();

            // This requires the CrmSvcUtil.exe code generator to create the class file.
            // Sample :: CrmSvcUtil.exe /out:XRMClass.cs /url:https://contoso.crm.dynamics.com/XRMServices/2011/Organization.svc /username:user.name@contoso.onmicrosoft.com /password:ThisIsYourPassword! /namespace:XRMClass /serviceContextName:XrmServiceContext  /codeCustomization:"Microsoft.Xrm.Client.CodeGeneration.CodeCustomization,Microsoft.Xrm.Client.CodeGeneration"
            XrmServiceContext xrm = new XrmServiceContext("xrm");

            // Setup the Connect context to the SP Server.           
            ClientContext ctx = SetupSPContext(SharePointCRMDocumentsSiteName, SharePointCRMDocumentsLoginName, SharePointCRMDocumentsPwd);

            // This is the path to the documents library in SharePoint to hold the attachments
            string sitePath = SharePointCRMDocumentsSiteName + "/" + Account.EntityLogicalName.ToLower();

            // Retrieve the Site record from CRM to get the relative path
            SharePointSite spSite = xrm.SharePointSiteSet.Where(n => n.Name == SharePointCRMSchedulingRequestSiteName).SingleOrDefault();
            
            // Here we build the URL with the site and path to the document library and folder to see if it already exists
            string relativeURL = GetSPFolderPathFromCRM(recordId, Account.EntityLogicalName, spSite.Id);

            // If it does not exist, we need to create a folder to hold the files for this record
            // Note: For simplicity, we are using the guid of the record, but you can use whatever you like.  
            //      CRM by default uses the record name, underscore, and the guid (e.g. Contoso_[record id guid] )
            //      When working with existing entity records and folders, you may need some more advanced code depending on how your 
            //      SharePoint integration with CRM is setup.
            if (string.IsNullOrEmpty(relativeURL))
            {
                CreateSPFolder(recordId, ctx, SharePointCRMSchedulingRequestSiteName, recordId.ToString().ToLower(), sitePath);
            }

            // Now we append the filename to the path
            string fullURLWithFileName = sitePath + "/" + recordId.ToString() + "/" + fileName;

            // Upload the file!
            UploadFileToSP(ctx, "account", fullURLWithFileName, fileContent);
           
                
        }

        public static ClientContext SetupSPContext(string documentLibrarySiteURL, string userName, string password)
        {
            // This builds the connection to the SP Online Server
            ClientContext clientContext = new ClientContext(documentLibrarySiteURL);
            SecureString secureString = new SecureString();
            foreach (char c in password.ToCharArray()) secureString.AppendChar(c);
            clientContext.Credentials = new SharePointOnlineCredentials(userName, secureString);
            Web web = clientContext.Web;
            clientContext.Load(web);
            clientContext.ExecuteQuery();
            return clientContext;
        }

        public static string GetSPFolderPathFromCRM(Guid regardingObjectId, string regardingObjectEntityName, Guid siteId)
        {
            // We query the SharePointDocumentLocation entity to get the parentid
            string returnValue = string.Empty;
            XrmServiceContext xrm = new XrmServiceContext("xrm");
            SharePointDocumentLocation parentLocationRecord = xrm.SharePointDocumentLocationSet
                .Where(d => d.ParentSiteOrLocation.Id == siteId)
                .OrderByDescending(d => d.ModifiedOn)
                .FirstOrDefault();
            if (parentLocationRecord == null) throw new Exception("parentLocationRecord not found, check CRM for SP Site!");

            // Now we check if the location has already been entered into CRM for this record.
            SharePointDocumentLocation documentLocation = xrm.SharePointDocumentLocationSet
                .Where(d => d.RegardingObjectId.Id == regardingObjectId)
                .Where(d => d.ParentSiteOrLocation.Id == parentLocationRecord.Id)
                .FirstOrDefault();

            // If it's there, then we return the path
            if (documentLocation != null)
            {
                returnValue = documentLocation.RelativeUrl;
            }
            else
            {
                // Document Location record does not exist, so let's create it.
                CreateSPDocumentLocationRecord(regardingObjectId, regardingObjectEntityName, parentLocationRecord);
                returnValue = string.Empty;
            }

            return returnValue;
        }

        private static void CreateSPDocumentLocationRecord(Guid regardingObjectId, string regardingObjectEntityName, SharePointDocumentLocation parentLocationRecord)
        {
            // We have to link up the document location with a new record in CRM.
            Microsoft.Xrm.Client.CrmConnection c = new Microsoft.Xrm.Client.CrmConnection("xrm");
            XrmServiceContext xrm2 = new XrmServiceContext(c);
            SharePointDocumentLocation newDocumentLocation = new SharePointDocumentLocation();
            newDocumentLocation.RelativeUrl = regardingObjectId.ToString().ToLower();
            newDocumentLocation.Name = regardingObjectEntityName + " :: " + regardingObjectId.ToString().ToLower();
            newDocumentLocation.RegardingObjectId = new Microsoft.Xrm.Client.CrmEntityReference(regardingObjectEntityName, regardingObjectId);
            newDocumentLocation.ParentSiteOrLocation = new Microsoft.Xrm.Client.CrmEntityReference(SharePointDocumentLocation.EntityLogicalName, parentLocationRecord.Id);
            xrm2.Create(newDocumentLocation);
        }

        public static void CreateSPFolder(Guid regardingObjectId, ClientContext ctx, string listName, string folderName, string folderUrl)
        {
            // Create a new folder in the SP document library
            // Note: For simplicity, we are using the guid of the record, but you can use whatever you like.  
            //      CRM by default uses the record name, underscore, and the guid (e.g. Contoso_[record guid] )
            //      When working with existing entity records and folders, you may need some more advanced code depending on how your 
            //      SharePoint integration with CRM is setup.

            Microsoft.SharePoint.Client.List list = ctx.Web.Lists.GetByTitle(listName);
            folderName = folderName.Trim();

            ListItemCreationInformation info = new ListItemCreationInformation();
            info.UnderlyingObjectType = FileSystemObjectType.Folder;
            info.LeafName = folderName.Trim();
            info.FolderUrl = folderUrl;

            Microsoft.SharePoint.Client.ListItem newItem = list.AddItem(info);
            newItem["Title"] = folderName.Trim();
            newItem.Update();
            ctx.ExecuteQuery();

        }

        public static void UploadFileToSP(ClientContext ctx, string listName, string fullUrlWithFileName, byte[] content)
        {
            //  Send the file content to SP
            Microsoft.SharePoint.Client.List list = ctx.Web.Lists.GetByTitle(listName);
            FileCreationInformation file1 = new FileCreationInformation();
            file1.Overwrite = true;
            file1.Url = fullUrlWithFileName;
            file1.Content = content;
            Microsoft.SharePoint.Client.File uploadFile = list.RootFolder.Files.Add(file1);
            ctx.Load(uploadFile);
            ctx.ExecuteQuery();
        }

        
    }

}





Tuesday, February 24, 2015

Updating Icons blows up Auditing?

UPDATE: Apparently this is a known function in CRM, except it doesn't make sense that it happens when you are updating Icons.  

Please take a moment and vote this up on Microsoft Connect: http://bit.ly/1ERbj0i

----------
I've been working for the past few weeks on the upgrade of a CRM v2011 instance to a v2015 instance.

One thing I noticed in CRM 2015 is that all of the icon images are flat and typically a monochromatic color.  Usually they are a Shade of Grey... the color, not the book/movie.  :)

So working with a colleague of mine, I asked him to make up some nice monochromatic icons to represent the 2 dozen or so custom entities we have.  He made up some nice icon files in 16x16 and 32x32 format, and to save time I asked him to go ahead and update the icons in our 2011 instance so we don't have to go through and do it during the final upgrade process.

Then a few hours later I get a Lync message from a CRM user asking what's happened to the Audit logs for a record?

My reply was... Huh?  Nothing that I know of?

So I check the record, and lo and behold, all the new value entries on the audit log are missing, replaced by an strange icon image indicating that it's not there.




After the Icons were updated...



I'm stumped...  I can see an entry where auditing was turned off, and it turned out that was when the icons were updated. It decided to turn off the auditing for a moment, and then back on, which caused this problem.  I can easily reproduce it over and over.

Anything updated after this time is fine, and gets audited properly.

So I did a search and the only thing I could find was this Forum post.  It's not exactly the same thing because they were intentionally turning auditing off and back on, but the result was the same.  All the 'new value' entries are messed up.

FYI... this problem also occurs in a totally clean, not upgraded, on-premise instance of CRM 2015 as well.  





I also updated my CRM 2011 to UR 18, just to be sure that this wasn't fixed in an update release.

I have opened a support case with Microsoft to see if they are aware of this issue, and if there is a fix.  I will update this blog entry if I get a fix.



Thursday, February 5, 2015

Visual Studio (VS) 2013 and CRM Explorer

I ran into this problem today after I needed to change the CRM URL on the Dev Server I am working with CRM 2015 on.

I had previously created both a Plugin project and a Web Resources project and used the CRM Explorer to browse these to extract items, setup new Plugin Steps, etc.

I was tweakign the CRM Dev server and discovered that I needed to modify the URL for the server in the Deployment Manager.

After doing so, my Visual Studio project stopped working correctly, and the CRM explorer would throw an error about 

"No Organization has been specified"

Did some searching and came across this forum post.

I figured it out: informations about CRM Server are stored in *.suo file. I just deleted this file, opened my solution in VS and entered all informations for CRM Server connection. The suo file was regenerated automatically and I can work now with my CRM solution

The answer was the *.suo file in the same directory as the VS solution file.  While the solution is closed, just delete or rename that file.  When you re-load Visual Studio next time, you may still see the error, but you can then reconnect to the new CRM server or URL.

Tuesday, January 6, 2015

CRM2013 : Reporting Extensions not installed?

I ran into this problem today when I had top stop the install of CRM 2013 at the end when it tried to put in the reporting extensions.

http://www.interactivewebs.com/blog/index.php/general-tips/crm-2013-reporting-is-not-enabled-message-in-crm-deployment-manager/

The original reason it wouldn't install was that the reporting services (SSRS) service account matched the same account that CRM was using for it's services.