Monday, 31 October 2016

XRMToolBox plugin to convert from FetchXML to C#/JavaScript string

Hello all,

Recently I had created a plugin for XrmToolBox that converts from FetchXML to C#/JavaScript string to help with Dynamics CRM development.
You can find screenshots of the tool here.

In case you want to use the tool (in case you are offline) and do not want to use the online version here, then these are the 2 ways to install the tool in your XrmToolBox:

1. As a Nuget package from the plugins store: Please search for the plugin with the name "Fetch XML To String Converter plugin". This is applicable only for the latest releases of XrmToolbox.

2. Directly install and save the DLL of this plugin in the plugin location used by XrmToolBox.
Please download the version (based on your version of XrmToolBox) from here.

Tuesday, 11 October 2016

Web application to convert FetchXML to C#/JavaScript string

Hi all,

Many times we have the requirement to convert the Fetch XML that we download from the Dynamics CRM advanced find window to strings that are valid for C#/ JavaScript so that we can use them in our Fetch expressions, custom views, etc. I have made a web tool that will easily allow you to perform the conversion without having to manually do find/replace and other operations.
The web tool can be found here: https://ace-racer.github.io/FetchXmlToString/


The tool is proudly hosted by Github pages!

Hope you find the tool helpful!

Thanks!!

Starting with PowerShell for Dynamics CRM

Hi all,

Recently I had a requirement to change a deployment setting in Dynamics CRM. The issue was that this setting could either be modified by updating a flag in the CRM database or using a powershell script but not from any convenient UI. In my quest to solve this issue, I uncovered how to get Powershell configured so that we can use the common Dynamics CRM cmdlets. I will describe those steps as well as give the script that will help to update obscure deployment settings for CRM Online (when there is no access to the CRM database)


Configuring Power Shell with Dynamics CRM: The steps in details are mentioned here.
Once Power Shell is configured, we will focus on the steps to update the deployment settings:

# on premises instance
$CRM_URL = "http://IP:PORT";
$ORG_NAME = "contosoprod";

#Obtain the credentials in the pop up
$cred = Get-Credential 

# Get the connection object (below will change based on deployment type - refer to above link)
$conn = Get-CrmConnection -ServerUrl $CRM_URL -Credential $cred -OrganizationName $ORG_NAME;

# add snap in
Add-PSSnapin Microsoft.Crm.PowerShell;

# get the present team settings 
$set = Get-CrmSetting -SettingType TeamSettings;

# update the required setting (not yet transacted to database)
$set.MaxEntitiesEnabledForAutoCreatedAccessTeams = 8;

# set the required setting (database updated)
Set-CrmSetting -Setting $set;

# view the updated settings
Get-CrmSetting -SettingType TeamSettings;

Here we have updated the MaxEntitiesEnabledForAutoCreatedAccessTeams property in the TeamSettings. However, there exist a host of settings that can be updated using the same procedure as described above. The full list can be found here.

Hope this starting tutorial helps you realize the potential of infusing Powershell with Dynamics CRM!

Let me know your views!

Thanks!

Plugin on associate disassociate of members in an N to N relation

Hi guys,

In this post I will walk through the steps to create a associate disassociate plugin in Dynamics CRM. The post here covers the steps. However, I will want t o give a more generic code as well as the steps that can be put in the register file.

The associate/ disassociate message are unlike other messages in that they are not specific to any entity but trigger globally for all entities. The below shows the steps in the CRM register file:

 <Step CustomConfiguration="" Name="AssociateUsers" Description="Post-Operation of AssociateDisassociateUsers" Id="9a550c63-b28b-e611-80d5-000d3aa06eb4" MessageName="Associate" Mode="Asynchronous" PrimaryEntityName="" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly">
              <Images />
            </Step>

            <Step CustomConfiguration="" Name="DisassociateUsers" Description="Post-Operation of AssociateDisassociateUsers" Id="9c550c63-b28b-e611-80d5-000d3aa06eb4" MessageName="Disassociate" Mode="Asynchronous" PrimaryEntityName="" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly">
              <Images />
            </Step>


Since the plugin will trigger for all associate-disassociate events, care must be taken to verify the relation that triggers the event as shown in the plugin code below:

 const string AssistantUsersRelationshipName = "contoso_systemuser_systemuser";

 if (context.MessageName == "Associate" || context.MessageName == "Disassociate")
            {
                if (context.InputParameters.Contains("Relationship"))
                {
                    var relationshipName = context.InputParameters["Relationship"].ToString();
                    var doesStartWithExpectedRelationName = relationshipName.ToLower(CultureInfo.CurrentCulture).StartsWith(AssistantUsersRelationshipName);
                    if (doesStartWithExpectedRelationName)
                    {
                        if ((context.InputParameters.Contains("Target") && context.InputParameters["Target"] is EntityReference))
                        {
                            // contains the user to which the records are associated
                            var targetEntityRef = (EntityReference)context.InputParameters["Target"];
                            var entityLogicalNameLower = targetEntityRef.LogicalName;
                            if (string.Compare(entityLogicalNameLower, "systemuser", StringComparison.CurrentCultureIgnoreCase) == 0)
                            {
                                if (context.InputParameters.Contains("RelatedEntities") &&
                                    context.InputParameters["RelatedEntities"] is EntityReferenceCollection)
                                {

                                    // get the source user from the assistant user
                                    var relatedEntities =
                                        context.InputParameters["RelatedEntities"] as EntityReferenceCollection;
                                    if (relatedEntities.Count > 0)
                                    {
                                        var relatedEntity = relatedEntities[0];
                                        // Your logic goes here
                                    }
                                }
                            }
                        }
                    }
                }
            }


The highlighted region code snippet that checks whether the relationship name that comes as part of the input parameters is as expected. The relationship above is a N:N relation between user and user record and another check (highlighted) is present that checks the logical name of the entity reference that comes with the input parameters.

The targetEntityRef variable contains the entity which is being associated/disassociated and the relatedEntity variable contains the entity reference to the source entity to/from which the entity is being associated/ disassociated.

Hope this helps you in your plugin development journey!
Cheers!

Sunday, 9 October 2016

Triggering plugin on adding and removing members from access teams

Hi all,

In this blog post I will give the code to obtain the required details in a plugin that triggers on adding and removing members from access team. Please refer here for details on the properties that come as part of the input parameters.

Here is the step configuration (that will go to the CRM register file):

<Step CustomConfiguration="" Name="PostTeamtemplateAddUserToRecordTeam" Description="Post-Operation of Team template AddUserToRecordTeam" Id="71b99ae3-488c-e611-80d5-000d3aa06eb4" MessageName="AddUserToRecordTeam" Mode="Asynchronous" PrimaryEntityName="teamtemplate" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly">
              <Images />
            </Step>

            <Step CustomConfiguration="" Name="PostTeamtemplateRemoveUserFromRecordTeam" Description="Post-Operation of Team template RemoveUserFromRecordTeam" Id="c71676f2-488c-e611-80d5-000d3aa06eb4" MessageName="RemoveUserFromRecordTeam" Mode="Asynchronous" PrimaryEntityName="teamtemplate" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly">
              <Images />
            </Step>

As can be seen from above, the primary entity for the plugin is the "TeamTemplate" entity.

The plugin code will look like the below:

if (context.MessageName == "AddUserToRecordTeam" || context.MessageName == "RemoveUserFromRecordTeam")
            {
                EntityReference dealEntityRef = null;
                if ((context.InputParameters.Contains("Record") && context.InputParameters["Record"] is EntityReference))
                {
                    var recordEntityRef = (EntityReference)context.InputParameters["Record"];
                    if (recordEntityRef.LogicalName == "opportunity")
                    {
                        dealEntityRef = recordEntityRef;
                    }
                }

                if (dealEntityRef != null)
                {
                    // do logic here
                }
            }

Here, I have only considered the "Record" input parameter but you can also consider the SystemUserId, TeamTemplateId parameters as required in your logic.

Let me know if you have any questions!

Happy CRMing! 

Saturday, 1 October 2016

Dynamics CRM on-premises plugin to upload documents to SharePoint on-premises

Hi all,

Some times we come across the requirement to upload documents to SharePoint programatically from within a Dynamics CRM plugin. In my last post I had shown how to upload documents to SharePoint using a console application. We are going to reuse the same code here, however there are some additional steps that we need to do here that will ensure that the plugin works as expected without throwing any exception.

These are the steps before we even start developing the plugin:
1. If the source files to be uploaded to SharePoint are in a folder on the server or a shared location,m the Network Services account should have "read" privilege on this folder. The asynchronous service in which this plugin will run will run under the context of the "Network Service" account and hence it requires access to this folder. This can be achieved by right clicking the folder and going to the Security tab and add "Network Service" account by giving it read permission.

2. The assembly containing the SharePoint integration plugin should not be deployed in the Sandboxed mode. This will ensure that the plugin can read from the file system and access the SharePoint Dlls (later). Since plugins to Dynamics CRM Online can only be deployed in the Sandboxed mode so this plugin will not work for CRM Online.

3. The SharePoint Dlls that will be used in the code need to be GACed in the target server where CRM is installed. It is simply achieved by running the GacUtil.exe command in the Developer tools command prompt (running in the admin mode) pointing to the SharePoint Dlls below:

a. Microsoft.SharePoint.Client.dll
b. Microsoft.SharePoint.Client.Runtime.dll

Additional details on GacUtil can be found here.

The actual plugin needs to contain this code (from the last post here) and taking some concepts from here:

/// <summary>
        /// The sharepoint default site URL configuration key
        /// </summary>
        private const string SharepointDefaultSiteUrlConfigKey = "SharePointDefaultSiteUrl";

        /// <summary>
        /// The sharepoint username configuration key
        /// </summary>
        private const string SharepointUsernameConfigKey = "SharePointUserName";

        /// <summary>
        /// The sharepoint password configuration key
        /// </summary>
        private const string SharepointPasswordConfigKey = "SharePointPassword";

        /// <summary>
        /// The sharepoint domain configuration key
        /// </summary>
        private const string SharepointDomainConfigKey = "SharePointDomain";

// code to be included in Plugin
var completePath = "C:\\Users\\anurag\\Desktop";
                var pricingDocuments = Directory.GetFiles(completePath, "Test*");
                if (pricingDocuments.Length > 0)
                {
                    // Attach the files obtained to the product
                    var defaultSiteUrl = Common.RetrieveConfigurationData(SharepointDefaultSiteUrlConfigKey, orgService);
                    var sharePointUser = Common.RetrieveConfigurationData(SharepointUsernameConfigKey, orgService);
                    var sharepointPassword = Common.RetrieveConfigurationData(SharepointPasswordConfigKey, orgService);
                    var sharepointDomain = Common.RetrieveConfigurationData(SharepointDomainConfigKey, orgService);
                    const string productLocationStr = "product";
                 
                    using (var clientContext = new ClientContext(defaultSiteUrl))
                    {
                        clientContext.Credentials = new NetworkCredential(sharePointUser, sharepointPassword, sharepointDomain);
                        Web web = clientContext.Web;

                        var allProductsFolder = web.GetFolderByServerRelativeUrl(defaultSiteUrl + "product");
                        clientContext.Load(allProductsFolder, i => i.Folders);
                        clientContext.ExecuteQuery();
                        var productFolders = allProductsFolder.Folders;                      
                        if (productFolders != null)
                        {
                            var requiredFolderName = productEntity.GetAttributeValue<string>("name") + "_" + productEntity.Id.ToString().Replace("-", string.Empty).ToUpper();                          
                            Folder requiredFolder = null;
                            try
                            {
                                requiredFolder =
                                    web.GetFolderByServerRelativeUrl(defaultSiteUrl + productLocationStr + "/" + requiredFolderName);
                                clientContext.Load(requiredFolder);
                                clientContext.ExecuteQuery();                              
                            }
                            catch (ServerException ex)
                            {
                                if (ex.ServerErrorTypeName == "System.IO.FileNotFoundException")
                                {
                                    // the required folder does not exist - so create it                        
                                    requiredFolder = allProductsFolder.Folders.Add(requiredFolderName);
                                    clientContext.Load(requiredFolder);
                                    clientContext.ExecuteQuery();
                                }
                                else
                                {
                                    throw;
                                }
                            }

                            foreach (var pricingDocumentWithLocation in pricingDocuments)
                            {
                                var fileName = Utility.GetFileName(pricingDocumentWithLocation);
                                var fileContents = File.ReadAllBytes(pricingDocumentWithLocation);
                                var fci = new FileCreationInformation();
                                fci.Content = fileContents;
                                fci.Url = fileName;
                                fci.Overwrite = true;
                                var fileToUpload = requiredFolder.Files.Add(fci);
                                clientContext.Load(fileToUpload);
                                clientContext.ExecuteQuery();
                            }
                        }
                    }


// helper class
 public static class Utility
    {
        public static string GetFileName(string fileLocation)
        {
            if (!string.IsNullOrWhiteSpace(fileLocation))
            {
                var indexOfLastSlash = fileLocation.LastIndexOf('\\');
                if (indexOfLastSlash >= 0)
                {
                    return fileLocation.Substring(indexOfLastSlash + 1);
                }
            }

            return string.Empty;
        }
    }


The above plugin code creates a SharePoint folder for each "product" record in Dynamics CRM, if the folder for that product does not already exists and uploads the documents that are found in the filesystem folder to that SharePoint folder. Also, note the naming used for the folders, it is the same way in which Dynamics CRM names the folder for each of the product record out of the box.

Hope it helps make it easy to write your own plugin when you have a similar requirement to implement.

Uploading files to SharePoint from Local system programatically

Hi guys,

In this post I am going to describe how to write a console application that will upload all files present in a particular directory into SharePoint.

You would need to install the SharePoint client Dlls to use the SharePoint 2010 SDK.
These Dlls are required and can be downloaded from here:
Microsoft.SharePoint.Client
Microsoft.SharePoint.Client.Runtime

The main method that gets the files from the location and uploads them into SharePoint is as below:
 public static void Main(string[] args)
        {
            var pricingDocuments = Directory.GetFiles("C:\\Users\\anurag\\Desktop");
            // Attach the files obtained to the product
            var defaultSiteUrl = "http://192.168.85.9/";
            var sharePointUser = "USER_NAME";
            var sharepointPassword = "PASSWORD";
            var sharepointDomain = "DOMAIN";
            const string baseFolderName = "product";

            using (var clientContext = new ClientContext(defaultSiteUrl))
            {
                clientContext.Credentials = new NetworkCredential(sharePointUser, sharepointPassword, sharepointDomain);
                Web web = clientContext.Web;

                var allProductsFolder = web.GetFolderByServerRelativeUrl(defaultSiteUrl + baseFolderName);
                clientContext.Load(allProductsFolder, i => i.Folders);
                clientContext.ExecuteQuery();
                var productFolders = allProductsFolder.Folders;
                Console.WriteLine("The number of folders inside is: " + productFolders.Count);
                if (productFolders != null)
                {
                   // file name will be of this format: Alpha_257B6BB3AFC54E13B5B68DE92C7F64D3
                    var requiredFolderName = "Alpha" + "_" + Guid.NewGuid().ToString().Replace("-", string.Empty).ToUpper();
                   
                    Folder requiredFolder = null;
                    try
                    {
                        requiredFolder =
                            web.GetFolderByServerRelativeUrl(defaultSiteUrl + baseFolderName + "/" + requiredFolderName);
                        clientContext.Load(requiredFolder);
                        clientContext.ExecuteQuery();                                              
                        Console.WriteLine("Created");
                    }
                    catch (ServerException ex)
                    {
                        if (ex.ServerErrorTypeName == "System.IO.FileNotFoundException")
                        {
                                // the required folder does not exist - so create it                          
                                requiredFolder = allProductsFolder.Folders.Add(requiredFolderName);
                                clientContext.Load(requiredFolder);
                                clientContext.ExecuteQuery();                          
                        }
                        else
                        {
                            // some other exception - yikes!
                            throw;
                        }
                    }

                    foreach (var pricingDocumentWithLocation in pricingDocuments)
                    {
                        var fileName = Utility.GetFileName(pricingDocumentWithLocation);
                        var fileContents = File.ReadAllBytes(pricingDocumentWithLocation);
                        var fci = new FileCreationInformation();
                        fci.Content = fileContents;
                        fci.Url = fileName;
                        fci.Overwrite = true;
                        var fileToUpload = requiredFolder.Files.Add(fci);
                        clientContext.Load(fileToUpload);
                        clientContext.ExecuteQuery();
                    }
                }
            }          
        }

The parts highlighted above need to be replaced according to the required SharePoint instance/ local file system location adhering to the format to avoid any exception.

Hope this helps!