Friday, 23 December 2016

Generic code to show SharePoint documents subgrid on main entity form in Dynamics CRM

Hello!

Many times we have faced the ask from our customers to show the SharePoint document sub-grid on the main form for an entity. I have come up with a generic code that shows the SharePoint sub-grid on the main form (taking only the tab name, entity schema name, record GUID and the IFrame name).

Please note that the code is only for the sub-grids created by using the SharePoint list component and as such it is not supported as we are hard-coding the list component parameters in the URL. So use it at your own risk:

/* Show the documents tab on the main entity form based on the entities that have been provided */
/* This method has dependency on XrmServiceToolkit.js */
common.showDocumentsGridOnMainEntityForm = function (documentGridTabName, entityLogicalName, recordGuid, documentGridIframeName) {
    if (Xrm && Xrm.Page && Xrm.Page.ui) {
        var CREATE_FORM_TYPE = 1;
        var formType = Xrm.Page.ui.getFormType();
        if (formType != CREATE_FORM_TYPE) {
            debugger;
            var sharePointSitesFetch = "<fetch distinct='false' mapping='logical' output-format='xml-platform' version='1.0'>" +
                "<entity name='sharepointsite'>" +
                "<attribute name='name'/>" +
                "<attribute name='parentsite'/>" +
                "<attribute name='relativeurl'/>" +
                "<attribute name='absoluteurl'/>" +
                "<attribute name='validationstatus'/>" +
                "<attribute name='isdefault'/>" +
                "<order descending='false' attribute='name'/>" +
                "<filter type='and'>" +
                "<condition attribute='isdefault' value='1' operator='eq'/>" +
                "<condition attribute='absoluteurl' operator='not-null'/>" +
                "</filter>" +
                "</entity>" +
                "</fetch>";
            var sharePointSiteRecords = XrmServiceToolkit.Soap.Fetch(sharePointSitesFetch);
            var sharePointBaseUrl = "";
            if (sharePointSiteRecords && sharePointSiteRecords.length > 0) {
                sharePointBaseUrl = sharePointSiteRecords[0].attributes["absoluteurl"].value;
            }

            var showDocumentsTab = false;
            if (sharePointBaseUrl) {
                var documentLocationsFetch = "<fetch distinct='false' mapping='logical' output-format='xml-platform' version='1.0'>" +
                    "<entity name='sharepointdocumentlocation'>" +
                    "<attribute name='name'/>" +
                    "<attribute name='regardingobjectid'/>" +
                    "<attribute name='parentsiteorlocation'/>" +
                    "<attribute name='relativeurl'/>" +
                    "<attribute name='absoluteurl'/>" +
                    "<order descending='false' attribute='name'/>" +
                    "<filter type='and'>" +
                    "<condition attribute='regardingobjectid' value='" + recordGuid + "' uitype='" + entityLogicalName + "' operator='eq'/>" +
                    "</filter>" +
                    "</entity>" +
                    "</fetch>";
                var documentLocationsForRecord = XrmServiceToolkit.Soap.Fetch(documentLocationsFetch);
                var documentLocationRelativeUrl = "";
                if (documentLocationsForRecord && documentLocationsForRecord.length > 0) {
                    documentLocationRelativeUrl = documentLocationsForRecord[0].attributes["relativeurl"].value;
                }

                var completeFolderUrl = "";
                if (documentLocationRelativeUrl) {
                    // Sample: http://192.168.85.9:8080/crmgrid/crmgridpage.aspx?langId=en-US&locationUrl=http%3a%2f%2f192.168.85.9%3a8080%2fcontoso_pricingproposal%2f_F66CBD47B6B8E61180D9000D3AA06EB4&pageSize=250
                    completeFolderUrl = sharePointBaseUrl + "crmgrid/crmgridpage.aspx?langId=en-US&locationUrl=" + sharePointBaseUrl + entityLogicalName + "/" + documentLocationRelativeUrl + "&pageSize=250";
                    console.log("The complete folder URL is: " + completeFolderUrl);
                    completeFolderUrl = encodeURI(completeFolderUrl);
                    console.log("The complete folder URL after encoding: " + completeFolderUrl);
                }

                if (completeFolderUrl) {
                    showDocumentsTab = true;
                }

                if (showDocumentsTab) {
                    if (Xrm.Page.ui.tabs) {
                        var documentGridTab = Xrm.Page.ui.tabs.get(documentGridTabName);
                        if (documentGridTab) {
                            documentGridTab.setVisible(true);
                            var documentIFrame = Xrm.Page.ui.controls.get(documentGridIframeName);
                            if (documentIFrame != null) {
                                documentIFrame.setSrc(completeFolderUrl);
                            }
                        }
                    }
                }
            }
        }
    }
}


Sample usage code:

/* Method to call common method to show sharepoint documents in the main entity form*/
pricingproposal.showDocumentsInMainform = function () {
    var recordGuid = Xrm.Page.data.entity.getId();
    if (recordGuid && common.isValidGuidHelper(recordGuid)) {
        common.showDocumentsGridOnMainEntityForm("tab_DocumentsTab", "contoso_pricingproposal", recordGuid, "IFRAME_Documents");
    }
}

As part of the configurations we need to create a tab in the main form inside which we will have an IFrame. These names will be part of the function parameter as shown above.

Let me know if you have any questions.

Merry Christmas and happy Dynamics!


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!

Sunday, 4 September 2016

Fetch records using Javascript using XrmToolkit in Dynamics CRM

During development in CRM, I have seen that many times the requirements can get too complicated to be achieved using Odata queries. Sometimes it just feels good to execute a fetch XML that we have formed using the advanced find and use the resultant data to perform further business logic.
XrmServiceToolkit (find it here!) is an invaluable tool that every Dynamics CRM developer should have in his kitty to make development easy. In the example I will show how I achieved a complicated requirement by using XrmServiceToolkit's fetch method in my Javascript web resource:

The requirement was to find out whether there was any active campaign associated with a custom entity (contoso_contosoentity) associated with opportunity product through a field called Signing entity on the form of opportunity product associated with the current opportunity. The custom entity (contoso_contosoentity) and campaign were related by a N:N relation. The objective was to show a notification to the user on the opportunity (deal) form when the conditions met.

var presentDealRecordId = Xrm.Page.data.entity.getId();

var fetchXml =
                                    "<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='true'>" +
                                        "<entity name='campaign'>" +
                                        "<attribute name='name' />" +
                                        "<order attribute='name' descending='false' />" +
                                        "<filter type='and'>" +
                                        "<condition attribute='statecode' operator='eq' value='0' />" +
                                        "</filter>" +
                                        "<link-entity name='contoso_campaign_contoso_contosoentity' from='campaignid' to='campaignid' visible='false' intersect='true'>" +
                                        "<link-entity name='contoso_contosoentity' from='contoso_contosoentityid' to='contoso_contosoentityid' alias='ag'>" +
                                        "<link-entity name='opportunityproduct' from='contoso_signingentityid' to='contoso_contosoentityid' alias='ah'>" +
                                        "<link-entity name='opportunity' from='opportunityid' to='opportunityid' alias='ai'>" +
                                        "<filter type='and'>" +
                                        "<condition attribute='opportunityid' operator='eq' uitype='opportunity' value='" + presentDealRecordId + "' />" +
                                        "</filter>" +
                                        "</link-entity>" +
                                        "</link-entity>" +
                                        "</link-entity>" +
                                        "</link-entity>" +
                                        "</entity>" +
                                        "</fetch>";

                                var associatedActiveCampaigns = XrmServiceToolkit.Soap.Fetch(fetchXml);
                                if (associatedActiveCampaigns && associatedActiveCampaigns.length > 0) {
                                    shouldDisplayNotification = true;
                                    campaignName = associatedActiveCampaigns[0].attributes["name"].value;
                                }
                            }

                            var dealAlertNotificationId = "dealAlertCampaign";
                            if (shouldDisplayNotification && campaignName != null) {
                                var message = "Please consider associating this deal or a deal product to the following campaign: " + campaignName;
                                Xrm.Page.ui.setFormNotification(message, "INFO", dealAlertNotificationId);
                            } else {
                                Xrm.Page.ui.clearFormNotification(dealAlertNotificationId);
                            }


I have highlighted the line where we make the call to the Fetch method (which internally uses the SOAP endpoint). I have also highlighted the line where I use the returned result set to perform some action (in this case to populate the campaign name to show to the user)

Note:
1. I took the fetch XML from the advanced find window and then removed the attributes that were not required to reduce the load of the service call.
2. This service call is performed in a synchronous manner, which may not give a very good experience to the end user. We can make it asynchronous by passing a callback function as the third parameter here:

var fetch = function (fetchCore, fetchAll, callback) {
        ///<summary>
        /// Sends synchronous/asynchronous request to do a fetch request.
        ///</summary>
        ///<param name="fetchCore" type="String">
        /// A JavaScript String containing serialized XML using the FetchXML schema.
        /// For efficiency, start with the "entity" node.
        /// </param>
        ///<param name="callback" type="Function">
        /// A Function used for asynchronous request. If not defined, it sends a synchronous request.
        /// </param>

Hope this helps you implement some complicated requirements in CRM!

Sunday, 28 August 2016

Using money fields in plugins in Dynamics CRM

Hi all,

I had faced some issue finding the correct way to update the money type fields in Dynamics CRM and I write this post so that you do not have to face the same issue in your development journey. As you would know when we create a field in Dynamics CRM of the "Currency" type it creates internally another field with schema name  ' "schema name of the original currency field"_base'. Basically in this _base field the currency value is stored in the base currency of the system and in the original field the value is stored in the user's chosen currency or organizational set currency. Hence, each record in Dynamics CRM has a Currency lookup field with schema name "transactioncurrency" to the transactioncurrency entity that contains the details of the currency when this record was created. Among other details this entity also contains the exchange rate between this currency and the base currency of the system.

So my requirement was to update the total fees field in opportunity when the "fees" field in one of the related Opportunity product entities was updated. Due to some security related requirement, we were not using roll-up field and needed to achieve the functionality using a plugin.

 decimal totalDealProductFees = 0;
 var dealProductsQuery = new QueryExpression("opportunityproduct");
 dealProductsQuery.ColumnSet = new ColumnSet(new string[] { "contoso_fees_base" });
 dealProductsQuery.Criteria.AddCondition("opportunityid", ConditionOperator.Equal, dealEntityRef.Id);
 EntityCollection dealProductsQueryResults = orgService.RetrieveMultiple(dealProductsQuery);                              
 if (dealProductsQueryResults != null && dealProductsQueryResults.Entities != null && dealProductsQueryResults.Entities.Count > 0)
 {
tracingService.Trace("The number of deal product records obtained is: " + dealProductsQueryResults.Entities.Count);
        foreach (var dealProduct in dealProductsQueryResults.Entities)
        {                
          var feesDealProduct = dealProduct.GetAttributeValue<Money>("contoso_fees_base");
                if (feesDealProduct != null)
                    {
                           totalDealProductFees += feesDealProduct .Value;
                    }                                                    
        }
 }


 var exchangeRate = GetCurrencyExchangeRate(orgService, dealEntity.Id);
 dealEntity["contoso_totalfees"] = new Money(totalDealProductFees * exchangeRate);
 dealEntity["contoso_totalfees_base"] = new Money(totalDealProductFees);
 orgService.Update(dealEntity);


private static decimal GetCurrencyExchangeRate(IOrganizationService service, Guid dealId)
 {
     Guid CurrencyId = Guid.Empty;
     decimal oppExchangeRate = 0;
         
     if (dealId != Guid.Empty)
        {
          Entity oppEntity = service.Retrieve("opportunity", dealId, new ColumnSet("transactioncurrencyid"));
         CurrencyId = oppEntity.GetAttributeValue<EntityReference>("transactioncurrencyid").Id;
        }
           
     if (CurrencyId != Guid.Empty)
        {
          Entity currencyEntity = service.Retrieve("transactioncurrency", CurrencyId, new ColumnSet("exchangerate"));
          oppExchangeRate = currencyEntity.GetAttributeValue<decimal>("exchangerate");
        }
           
     return oppExchangeRate;
 }


The GetCurrencyExchangeRate method gets the currency exchange rate by obtaining the relevant fields from the opportunity entity and the transactioncurrency entities as discussed in the theory above.

As you will observe the currency fields are assigned their value by assigning to them a Money object, which has a constructor that takes a decimal value. We assign the actual computed value to the base field in the deal (opportunity) record and the value that is shown to the user is updated with the the actual value multiplied by the required exchange rate as obtained by the method discussed above.

Thus by following the above approach we can ensure that the currency values are in sync as required by the platform.

Hope this helps and feel free to leave your comments.
Thanks and Happy Dynamics!

Monday, 15 August 2016

Unit testing plugins in Dynamics CRM using Visual studio unit testing framework

Hi all,

In this post I will discuss the code that you can use to unit test your Dynamics CRM plugins using Visual studio unit testing framework.
You would first need to create the fake libraries for all the CRM SDK libraries as well as mscorlib and system DLLs. Make sure that the version matches with the version of the DLLs being used in the code. I have CRM 2015 SDK fakes and mscorlib/system DLL fakes here.

You can define a helper class with the following helper methods:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Fakes;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Fakes;

namespace Contoso.CRM.Unittests
{
    public class HelperMethodsContainer
    {
        /// <summary>
        /// Moles the plugin variables.
        /// </summary>
        /// <param name="serviceProvider">The service provider.</param>
        /// <param name="pluginContext">The plugin context.</param>
        /// <param name="organizationService">The organization service.</param>
        /// <param name="stageNumber">The stage number.</param>
        /// <param name="messageName">Name of the message.</param>
        public static void MolePluginVariables(
            StubIServiceProvider serviceProvider,
            StubIPluginExecutionContext pluginContext,
            StubIOrganizationService organizationService,
            int stageNumber,
            string messageName)
        {
            var serviceFactory = new StubIOrganizationServiceFactory();
            var tracingService = new StubITracingService();
            if (serviceProvider != null)
            {
                serviceProvider.GetServiceType = type =>
                {
                    if (type == typeof(IPluginExecutionContext))
                    {
                        return pluginContext;
                    }
                    else if (type == typeof(ITracingService))
                    {
                        return tracingService;
                    }
                    else if (type == typeof(IOrganizationServiceFactory))
                    {
                        return serviceFactory;
                    }

                    return null;
                };
            }

            pluginContext.DepthGet = () => 1;
            pluginContext.UserIdGet = () => new Guid();
            pluginContext.MessageNameGet = () => messageName;
            pluginContext.StageGet = () => stageNumber;
            pluginContext.InitiatingUserIdGet = () => new Guid();
            pluginContext.CorrelationIdGet = () => new Guid();
            pluginContext.PrimaryEntityIdGet = Guid.NewGuid;
            pluginContext.IsInTransactionGet = () => true;
            serviceFactory.CreateOrganizationServiceNullableOfGuid = t1 => organizationService;
            tracingService.TraceStringObjectArray = Trace;
        }

        /// <summary>
        /// Sets the mole for the Entity
        /// </summary>
        /// <param name="entityName">The LogicalName of the entity</param>
        /// <param name="attributeValues">The attributes of the entity</param>
        /// <param name="context">Object of type SIPluginExecutionContext</param>
        /// <returns>Object of type Entity</returns>
        public static Entity MoleEntity(string entityName, Dictionary<string, object> attributeValues, StubIPluginExecutionContext context)
        {
            var entity = new Entity(entityName);
            entity.Attributes = new AttributeCollection();

            if (attributeValues != null)
            {
                foreach (string key in attributeValues.Keys)
                {
                    entity.Attributes.Add(key, attributeValues[key]);
                }
            }

            if (context != null)
            {
                context.PrimaryEntityNameGet = () => entityName;
            }

            entity.Id = Guid.NewGuid();

            return entity;
        }

/// <summary>
        /// Moles the PluginContext.InputParameters
        /// </summary>
        /// <param name="context">the mole of the IPluginExecutionContext</param>
        /// <param name="inputParameterCollection">Object of type System.Dictionary</param>
        public static void MolePluginInputParameters(StubIPluginExecutionContext context, Dictionary<string, object> inputParameterCollection)
        {
            if (inputParameterCollection != null)
            {
                var parameterCollection = new ParameterCollection();
                foreach (var key in inputParameterCollection.Keys)
                {
                    parameterCollection.Add(key, inputParameterCollection[key]);
                }

                if (context != null)
                {
                    context.InputParametersGet = () => parameterCollection;
                }
            }
        }
   }
}

The first method "moles" the basic plugin variables which include the plugin depth, userID, message, stage, etc. The initial values can be anything unless they are required in the test logic.

The second method is used to mole the primary entity for the plugin (the entity which triggers the plugin)

The third method is used to set the input parameters for the plugin. This includes the "Target" parameter for a create/update plugin, etc.

In order to perform the actual test, we need to use the above methods to mole the basic plugin inputs. After that we also need to provide the implementations for the fake methods that will be invoked from the plugin. This is the standard way in which fakes framework works, where we provide the delegate that imitates what the function is expected to return.

Here is an example of a test class:

using System;
using System.Collections.Generic;
using System.Fakes;
using ContosoCRM.Plugins;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Fakes;
using Microsoft.Xrm.Sdk.Query;

namespace ContosoCRM.Unittests
{
    [TestClass]
    public class PostEmailCreateTests
    {
        [TestMethod]
        public void PostEmailCreateCreateTaskWhenConditionsMeet()
        {
            var postEmailCreatePlugin = new PostEmailCreate();
            var serviceProvider = new StubIServiceProvider();
            var pluginContext = new StubIPluginExecutionContext();
            var organizationService = new StubIOrganizationService();

            // Mole the basic Plugin objects
            HelperMethodsContainer.MolePluginVariables(serviceProvider, pluginContext, organizationService, 40, "Create");
            var subjectField = "Alpha beta " + "test suffix";          
            var entityAttributes = new Dictionary<string, object>();
            entityAttributes.Add("subject", subjectField);
            entityAttributes.Add("regardingobjectid", new EntityReference("opportunity", Guid.NewGuid()));
            entityAttributes.Add("description", "Hello world!");  
            Entity appointmentEntity = HelperMethodsContainer.MoleEntity("email", entityAttributes, pluginContext);

            // set the entity in the parameter collection
            var parameterAttributes = new Dictionary<string, object>();
            parameterAttributes.Add("Target", appointmentEntity);
            HelperMethodsContainer.MolePluginInputParameters(pluginContext, parameterAttributes);

            organizationService.CreateEntity = entity => Guid.NewGuid();
            organizationService.RetrieveStringGuidColumnSet = (s, guid, arg3) =>
            {
                var dealEntity = new Entity("opportunity");
                dealEntity.Id = Guid.NewGuid();
                dealEntity.Attributes.Add("name", "Alpha oppty");
                dealEntity.Attributes.Add("ownerid", new EntityReference("systemuser", Guid.NewGuid()));
                return dealEntity;
            };

            organizationService.RetrieveMultipleQueryBase = delegate(QueryBase query)
            {
                var entityCollection = new EntityCollection();
                Entity entity;
                if (query is QueryExpression && (query as QueryExpression).EntityName == "contoso_configuration")
                {
                    entity = new Entity("contoso_configuration")
                    {
                        Attributes = new AttributeCollection
                        {
                            { "contoso_name", "Alpha" },
                            { "contoso_value", "4"}
                        }
                    };
                    entityCollection.Entities.Add(entity);
                }
                if (query is QueryExpression && (query as QueryExpression).EntityName == "contoso_activitycategory")
                {
                    entity = new Entity("contoso_activitycategory")
                    {
                        Attributes = new AttributeCollection
                        {
                            { "contoso_name", "Validation" }                          
                        }
                    };
                    entityCollection.Entities.Add(entity);
                }

                return entityCollection;
            };
            postEmailCreatePlugin.Execute(serviceProvider);
        }
   }
}


The above unit test is for a plugin called "PostEmailCreate" which creates a task record whenever an email record is created in Dynamics CRM based on some conditions (like the subject of the email and its regarding object).

In the above it can be seen that delegates are provided to imitate the results of a retrieve organization service call ( organizationService.RetrieveStringGuidColumnSet ), a retrieve multiple organization service call ( organizationService.RetrieveMultipleQueryBase  ) and a create organization request (organizationService.CreateEntity). It can also be seen that different values are being returned based on some conditions (like which entity triggered the call, etc.) in the retrieve calls.

I hope with the above you get a general idea on how to write unit tests for your plugins that you write in Dynamics CRM. Let me know in the comments if you would like to know more about unit testing your plugins in Dynamics CRM.

Happy Dynamics CRM!!

Retrieve master entity record values using C#

Hi all,

In continuation of my previous post here: http://musingsofasoftwareenthusiast.blogspot.in/2016/08/retrieve-data-from-configuration-record.html, here I will discuss how we can retrieve any record with the specified column set and specified search criteria by creating a generic method. In particular, this will be extremely useful to retrieve the values from "Master" entity record. A "Master" entity is a entity whose records are less likely to be created/modified/deleted when the Dynamics CRM system is being used. Also, master records will have a unique value like its name by which we can identify the record.

Here is the generic method:

/// <summary>
        /// Retrieves the entity by unique values.
        /// </summary>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="queryColumns">The query columns.</param>
        /// <param name="requiredColumns">The required columns.</param>
        /// <param name="service">The service.</param>
        /// <returns>The entity as per the provided parameters</returns>
        public static Entity RetrieveEntityByUniqueValues(string entityName, Dictionary<string, object> queryColumns, List<string> requiredColumns, IOrganizationService service)
        {
            if (!string.IsNullOrWhiteSpace(entityName) && queryColumns != null && requiredColumns != null &&
                service != null)
            {
                var query = new QueryExpression(entityName);
                query.ColumnSet = new ColumnSet();
                foreach (var requiredColumn in requiredColumns)
                {
                    query.ColumnSet.AddColumn(requiredColumn);
                }

                foreach (var queryColumn in queryColumns)
                {
                    query.Criteria.AddCondition(queryColumn.Key, ConditionOperator.Equal, queryColumn.Value);
                }

                var recordsCollection = service.RetrieveMultiple(query);
                if (recordsCollection != null && recordsCollection.Entities != null &&
                    recordsCollection.Entities.Count > 0)
                {
                    return recordsCollection.Entities[0];
                }
            }

            return null;
        }

Here is how the method is called:

// get the validation activity category record
var validationActivityCategoryRecord =
Common.RetrieveEntityByUniqueValues(
"contoso_activitycategory",
new Dictionary<string, object>()
        {
           {"contoso_name", "Validation"}
        },
        new List<string>()
        {
           "contoso_name"
        },
        orgService);

Here is a walk through of the parameters that the method expects and how it works:

The first parameter is the name of entity that is expected.

The second parameter is a dictionary of the columns that will be part of the conditional expression in the query expression. As can be seen from the code above it is doing an equality check with the value provided. In the invocation above, I am passing the primary field of this entity (contoso_name) and providing the expected value as "Validation".

The third parameter is the list of columns that are expected from the entity being retrieved. Sometimes, we might not need any value as such, say when we are assigning the retrieved value to an entity reference field, so we can give an empty list (or the field that is part of the condition expression).

The last parameter is the organization service which is required to perform the platform operation.

I hope the above snippets help you to build up your Dynamics CRM Utility library!
Happy Dynamics CRMing!


Retrieve data from configuration record using C# in Dynamics CRM

Hi guys,

In continuation of my previous blog here: http://musingsofasoftwareenthusiast.blogspot.in/2016/08/retrieve-data-from-configuration.html, here I will discuss (and give the code) to retrieve data from configuration record using C#. In particular, I would keep this code in a Common.cs file, so that the methods in this file can be accessed by all plugins/workflow assemblies.

/// <summary>
        /// Get the value of the configuration record corresponding to the key
        /// </summary>
        /// <param name="configurationKeyName">configuration Key Name</param>
        /// <param name="service">IOrganization service</param>
        /// <returns>The value of the configuration record for the supplied key</returns>
        public static string RetrieveConfigurationData(string configurationKeyName, IOrganizationService service)
        {
            Entity configurationEntity = null;
            EntityCollection configurationEntityCollection = null;          
                QueryExpression qE = new QueryExpression("contoso_configuration");
                qE.ColumnSet = new ColumnSet("contoso_value");
                qE.Criteria.AddCondition(new ConditionExpression("contoso_name", ConditionOperator.Equal, configurationKeyName));
                if (service != null)
                {
                    configurationEntityCollection = service.RetrieveMultiple(qE);
                }

                if (configurationEntityCollection != null && configurationEntityCollection.Entities != null && configurationEntityCollection.Entities.Count > 0)
                {
                    configurationEntity = configurationEntityCollection.Entities[0];
                }
                else
                {
                    throw new InvalidPluginExecutionException("There is no configuration record with the name: " + configurationKeyName);
                }
           
            string configurationEntityValue = string.Empty;
            if (configurationEntity != null)
            {
                configurationEntityValue = configurationEntity.GetAttributeValue<string>("contoso_value");
            }

            return configurationEntityValue;
        }

I have used late binding here, so that names of the entity and fields will be as per your organization.

Also, note that unlike the Javascript example in my earlier post here we are using the all small case schema name to refer to the names of the entity and the fields.

Let me know if this helps by giving your feedback!
Happy Dynamics CRMing!

Retrieve data from configuration records in Dynamics CRM using JavaScript

In most of my projects, we have a configuration record in Dynamics CRM from where we retrieve configuration values. Here is a code snippet that will retrieve the configuration data based on the name supplied.

var common = common || {};

/*
Purpose : Get the configuration record value whose name has been provided
*/
common.getValueFromConfigurationRecordBasedOnName = function(configurationRecordName, isAsync) {
    if (configurationRecordName) {
        console.log("The configuration record name is: " + configurationRecordName);
        var oDataSet = "contoso_configuration";
        var options = "$select=contoso_Value&$filter=contoso_name eq '" + configurationRecordName + "'";
        var configurationDataRecordValue;
        if (isAsync == null) {
            isAsync = false;
        }
        SDK.REST.retrieveMultipleRecords(oDataSet, options, function (response) {
            configurationDataRecordValue = common.getValueFromConfigurationRecordBasedOnNameHelper(response);
        }, SDK.REST.errorCallback, function () {
            console.log("Retrieve multiple call for retrieving configuration record data completed");
        }, isAsync);
        return configurationDataRecordValue;
    } else {
        console.log("The configuration record name has not been provided.");
        return null;
    }
}

/*
Purpose : Helper function to process the response to get the configuration record value whose name has been provided
*/
common.getValueFromConfigurationRecordBasedOnNameHelper = function(response) {
    if (response) {
        console.log("The number of responses retrieved is: " + response.length);
        if (response.length > 0) {
            return response[0].contoso_Value;
        } else {
            console.log("There is no configuration data record with the given name");
        }
    } else {
        console.log("There is null response retrieved as part of the ODATA call.");
    }

    return null;
}

Here I have used the SDK.REST JavaScript library (a JS file) (find it here: https://msdn.microsoft.com/en-us/library/gg334427(v=crm.7).aspx#BKMK_SDKREST ) which also comes with the SDK. This helps avoid writing code to trigger the AJAX request while making the ODATA query. I have added an additional parameter called "isAsync" that mentions whether the request needs to be asynchronous or not by modifying the method in the SDK.REST file itself. This is required as in some cases we need to retrieve the data in a synchronous manner to drive the correct behavior.

This is the updated method declaration:

retrieveMultipleRecords: function (type, options, successCallback, errorCallback, OnComplete, isAsync) {
        ///<summary>
        /// Sends an asynchronous request to retrieve records.
        ///</summary>
        ///<param name="type" type="String">
        /// The Schema Name of the Entity type record to retrieve.
        /// For an Account record, use "Account"
        ///</param>
        ///<param name="options" type="String">
        /// A String representing the OData System Query Options to control the data returned
        ///</param>
        ///<param name="successCallback" type="Function">
        /// The function that will be passed through and be called for each page of records returned.
        /// Each page is 50 records. If you expect that more than one page of records will be returned,
        /// this function should loop through the results and push the records into an array outside of the function.
        /// Use the OnComplete event handler to know when all the records have been processed.
        /// </param>
        ///<param name="errorCallback" type="Function">
        /// The function that will be passed through and be called by a failed response.
        /// This function must accept an Error object as a parameter.
        /// </param>
        ///<param name="OnComplete" type="Function">
        /// The function that will be called when all the requested records have been returned.
        /// No parameters are passed to this function.
        /// </param>
        ///<param name="isAsync" type="Boolean">
        /// Whether the operation should be asynchronously performed or not
        /// </param>
        this._stringParameterCheck(type, "SDK.REST.retrieveMultipleRecords requires the type parameter is a string.");
        if (options != null)
            this._stringParameterCheck(options, "SDK.REST.retrieveMultipleRecords requires the options parameter is a string.");
        this._callbackParameterCheck(successCallback, "SDK.REST.retrieveMultipleRecords requires the successCallback parameter is a function.");
        this._callbackParameterCheck(errorCallback, "SDK.REST.retrieveMultipleRecords requires the errorCallback parameter is a function.");
        this._callbackParameterCheck(OnComplete, "SDK.REST.retrieveMultipleRecords requires the OnComplete parameter is a function.");

        var optionsString;
        if (options != null) {
            if (options.charAt(0) != "?") {
                optionsString = "?" + options;
            }
            else { optionsString = options; }
        }
        var req = new XMLHttpRequest();
        req.open("GET", this._ODataPath() + type + "Set" + optionsString, isAsync);
        req.setRequestHeader("Accept", "application/json");
        req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
        req.onreadystatechange = function () {
            if (this.readyState == 4 /* complete */) {
                req.onreadystatechange = null;
                if (this.status == 200) {
                    var returned = JSON.parse(this.responseText, SDK.REST._dateReviver).d;
                    successCallback(returned.results);
                    if (returned.__next != null) {
                        var queryOptions = returned.__next.substring((SDK.REST._ODataPath() + type + "Set").length);
                        SDK.REST.retrieveMultipleRecords(type, queryOptions, successCallback, errorCallback, OnComplete);
                    }
                    else { OnComplete(); }
                }
                else {
                    errorCallback(SDK.REST._errorHandler(this));
                }
            }
        };
        req.send();
    }

I have highlighted the changes made to the original retrieve multiple method above wherever the isAsync parameter is used.

Here is how to call the original retrieve configuration method:

var incrementValue = common.getValueFromConfigurationRecordBasedOnName("increment", false);



Additionally, there is also this method that will convert the configuration data value into an array (comma character in the configuration data value acts as the delimiter in the array)

/*
Purpose     : Retrieve configuration data and convert coma seprated string into an array after trimming the white spaces at the begining and end.
*/
common.getConfigurationDataConvertedToArray = function (configName) {
    var configRecord = common.getValueFromConfigurationRecordBasedOnName(configName, false);
    var configRecordArray = [];
    if (configRecord) {
        console.log("getConfigurationDataConvertedToArray() : Value of configuration record is " + configRecord);
        configRecordArray = configRecord.split(",");
        console.log("The number of items in the record: " + configRecordArray.length);
        for (var i = 0; i < configRecordArray.length; i++) {
            if (configRecordArray[i]) {
                configRecordArray[i] = configRecordArray[i].trim();
            }
        }
    } else {
        console.log("getConfigurationDataConvertedToArray() : configuration data is null.");
    }
    return configRecordArray;
}


Let me know if the above code snippets help you. Happy Dynamics CRMing!!!