Dec 16, 2019

Pitfall in bulk edit

Bulk edit is a very useful mechanism in Dynamics CRM/365 platform. This simply the ability of modifying more than one value at once. Anyway, there is a hidden pitfall where many users face issues. In this article I am simply trying to illustrate this as below;

For example I am trying to update below 5 records. So I am selecting them and clicking edit.


So I get below form with relevant fields. Suppose I only need to update Business Phone. So I only have to input new value and save. Please notice, we don't see any value for other fields.


Now I see our new value has been updated in all 5 records which is the expected out come. Pretty cool.


Now try to do the same operation (select all the records and click edit). This time I am populating Account Name lookup field. Then I change my mind and remove that value.


Once remove we can only see Business Phone field with our new values.


Once save, you will notice all the Company Name fields have been vanished !


Obviously, this is not a good user experience. Please make sure you understand this behavior before jump into Bulk Edit feature. If you are a trainer, please make sure you educate your audience with this tricky behavior. Hopefully, Microsoft will fix this in a coming release. Until then, play safe!

Nov 21, 2019

Identify Create Form and Update Form in Business Rules

It is recommended to use Business Rules to replace the functionalities of JavaScript. Yet, BRs got a lot of limitations, but it can be used for some of the most used aspects like controlling the UI of forms.

Anyway, one of the cool things we did using JS is controlling field visibility and business required levels based on Create Form and Update Form. There is a simple way of achieving the same with BR.

What we do here is checking the CreateOn date. Obviously, we don’t have a value in this field till save the record. Below simple condition shows how the Description field is hidden for Create Form and showing for Update Form.


Caution
Anyway, there is one trick here. Business Rule will only work if CreateOn field is present in the form. What we need to do is, just get the CreateOn into form, but set Visible by Default = No to hide it.

Oct 8, 2019

Passing Unsecure Configurations to a Plugin

We discussed how to pass Configurations to a JavaScript in a previous post. Click this to read that.

Lets see how we can pass Configurations to a Plug-in. In this scenarios, we'll see how we pass some Key Value pairs to a plug-in written for Lead entity.. We are going to store these values in a XML format within Unsecure Configuration section in registered plugin step.


This is the XML data format.

<leadConfig>
  <setting name="RegionCode" value="000X23AA55" />
  <setting name="IntegrationKey" value="1200-6753-0980-0901" />
</leadConfig>

Here is the Plug-in code that reads above values to be used in whatever the logic within the Plug-in. Interestingly, you will notice how we use a constructor class where configurations are being read within. Then we use GetUnsecureConfigValue() method to read each value in the XML.

using System;
using Microsoft.Xrm.Sdk;
using System.ServiceModel;
using System.Xml;

namespace TrialPlugin
{
    public class LeadPreCreate : IPlugin
    {
        private string UnsecureConfig { get; set; }

        public LeadPreCreate(string unsecureConfig, string secureConfig)
        {
            if (string.IsNullOrEmpty(unsecureConfig))
                throw new InvalidPluginExecutionException("Plugin Configuration missing.");
            UnsecureConfig = unsecureConfig;
        }

        public void Execute(IServiceProvider serviceProvider)
        {
            try
            {
                IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
                if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
                {
                    Entity TargetEnt = (Entity)context.InputParameters["Target"];
                    if (TargetEnt.LogicalName != "lead")
                        return;

                    IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                    IOrganizationService service = factory.CreateOrganizationService(context.UserId);

                    string regionCode = GetUnsecureConfigValue("RegionCode");
                    //string regionCode = Regex.Replace(GetUnsecureConfigValue("RegionCode"), @"\t|\n|\r", "").Replace(" ", String.Empty);
                    string integrationKey = GetUnsecureConfigValue("IntegrationKey");
                    //string regionCode = Regex.Replace(GetUnsecureConfigValue("IntegrationKey"), @"\t|\n|\r", "").Replace(" ", String.Empty);

                    throw new InvalidPluginExecutionException("RegionCode: " + regionCode + ", IntegrationKey: " + integrationKey);
                }
            }
            catch (FaultException<OrganizationServiceFault> e)
            {
                throw e;
            }
        }
        
        private string GetUnsecureConfigValue(string key)
        {
            const string XPATH = "leadConfig/setting[@name='{0}']";
            string configVal = string.Empty;
            try
            {
                var xmlDoc = new XmlDocument();
                xmlDoc.LoadXml(UnsecureConfig);
                var node = xmlDoc.SelectSingleNode(string.Format(XPATH, key));
                return node == null ? configVal : node.Attributes["value"].Value;
            }
            catch
            {
                return configVal;
            }
        }
    }
}

Caution
If you have lengthy values in XML, there is a chance that line-breaks and spaces being added without your intention. In such cases, you may use some Regular Expression functions to omit those. For above code, I have read the configuration data in two ways and commented out one. If explained issue likely to be hitting your situation, you may use the commented line instead of what is used.

Anyway, this method of passing configuration data is can be used to keep parameters differently for different environments. What is important to know is solution imports are overriding these vales.

Sep 26, 2019

Passing parameters to JavaScripts

Click this to see how parameters can be passed to Plug-ins.

Passing parameters to JavaScript is possible in Dynamics 365. When calling a particular method, we can see a way to pass whatever the parameters as below, apart from execution context as below.


Anyway, I find its pretty good since we can pass complex objects as Jason objects such as below;

{
    "type": "external",
    "isTaxInclude": true,
    "industryCode": 500,
        "profile": {
        "authority": "ABC Corp",
            "proficencyLevel": "Advanced",
            "jobCodes": [
                "GGG11",
                "ABX00",
                "XXY87"
            ],
        "noOfContractors":120
    }
}

If you go to debug (ad text debugger; to the code and perform operation after pressing F12) mode you will see how easy to access the different attributes with the JavaScript. Check how I see it once above Json object is passed;



If you are working with much complex Json objects, JsonPathFinder or JSONPath Finder chrome extension can help you sort the different values.

Passing parameters to JavaScript like this is particularly useful in below scenarios;

1) Same script to be used in different Forms with different parameters
2) Same script to be used in different Entities with different parameters

Anyway, one thing to keep in mind is these parameters can't be different in different environments. This is because, if a Solution is deployed with same Form, these parameters are being overridden with what is in the Solution. In other terms, this technique is NOT suitable to keep environmental specific variables.

One way to keep environmental variables is by checking the URL to determine the environment and load the variable accordingly. 

Sep 17, 2019

Programmatically populate Word Document Template

Ability to use Word document templates is one of the very useful features in Dynamics 365.

Here it is explained how to do it without any complicated steps.

Anyway, sometimes we may need to populate these templates programmatically. Below is the code snippet to do that using SetWordTemplate message.

OrganizationRequest req = new OrganizationRequest("SetWordTemplate");
req["Target"] = new EntityReference("account", new Guid("aaa19cdd-88df-e311-b8e5-6c3be5a8b200"));
req["SelectedTemplate"] = new EntityReference("documenttemplate", new Guid("9d6916e4-1033-4e03-a0e3-d15a5b133a9a"));
//if its a personal Template
//req["SelectedTemplate"] = new EntityReference("personaldocumenttemplate", new Guid("262032ac-13d9-e911-a975-000d3a37f8b9"));
svc.ExecuteCrmOrganizationRequest(req);

Anyway, this message manages to create the Document and attach to the relevant record as a Note. If its required to use this for other way, it may need to retrieve the document from the Note.

Sep 1, 2019

Is Workflow a thing of past ?

When you start working with Dynamics 365 CE Unified Interface, one of the things you would notice is there is no menu item to see available On Demand Workflows against a record.

This happens because this option is set to No by default. Below is the place (Settings > Administration > System Settings > Customization tab) to switch it Yes.


Once this is switched to Yes, new menu item will be visible called Flow (not Workflows!) as below, that contains relevant On Demand workflows. 


By the way, why this menu area called Flow? Well that's interesting. Flow is going to be the successor of good old Workflows! That's what Microsoft is recommending. As a evidence, when try to create a new workflow, you can now see it is pushing the user to move to Flow as below;


Is Workflow a thing of past ?

Aug 29, 2019

Connect to WebApi using ClientId and Secret

We can now register our Dynamics 365 CE in Azure Active Directory, so that platform can be accessed through different client applications using OAuth. Idea behind is client applications can securely access WebApi using just Client Id and Secret.

Below is a code snippet with C# in .NET Framework 4.6.2 to achieve it.

Click here to see how to register D365 CE in Azure

Only two NuGet packages required. (Please note one of them are not in latest version)


using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;


namespace ConnectWebApiWithClientId
{
    class Program
    {
        static void Main(string[] args)
        {
            Task.WaitAll(Task.Run(async () => await Auth()));
        }

        public static async Task Auth()
        {
            string url = "https://SumeTest.crm.dynamics.com";
            string clientId = "5d83f9es-a577-4s01-ab9b-9513e39k970c";
            string secret = "MvrHJ2T2YK7NabYFRSOfrEqLMME/1OMW8n6sVBA7zxI=";
            string apiVersion = "9.1";

            try
            {
                var userCredential = new ClientCredential(clientId, secret);
                string webApiUrl = $"{url}/api/data/v{apiVersion}/";

                var authParameters = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(webApiUrl)).Result;

                var authContext = new AuthenticationContext(authParameters.Authority, false);
                var authResult = await authContext.AcquireTokenAsync(url, userCredential);
                var authHeader = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

                using (var client = new HttpClient())
                {
                    client.BaseAddress = new Uri(webApiUrl);
                    client.DefaultRequestHeaders.Authorization = authHeader;

                    // Use the WhoAmI function
                    var response = client.GetAsync("WhoAmI").Result;
                    if (response.IsSuccessStatusCode)
                    {
                        Console.WriteLine("Authenticated Successfully");
                        Console.WriteLine("Environement : {0}", url);
                        Console.WriteLine();
                    }
                    else
                    {
                        Console.WriteLine("The request failed with a status of '{0}'", response.ReasonPhrase);
                    }
                }
            }
            catch (Exception ex)
            {
                throw ex.InnerException;
            }
            Console.WriteLine("Click Enter to Exit Application");
            Console.ReadLine();
        }
    }
}

Hope this helps.

Aug 20, 2019

Programmatical formatting of System Job Error string

When analyzing the errors in Dynamics 365 CE, we usually need to check System Jobs. In some cases, it may be required to create file (in most cases a Excel/ csv) with errors for further analysis. In such cases, it would be a troublesome exercise when it comes to handle the error message programmatically because they contain all kinds of error-prone characteristics such as spaces, many characters those become painful especially if you going to create importable file.

For example, consider below typical error message;

[Mxd.Crm.Workflows.Integration: Mxd.Crm.Workflows.Integration.CreateSalesData]
[Mxd.Crm.Workflows.Integration (1.0.0.0): Mxd.Crm.Workflows.Integration.CreateSalesData]

Correlation Id: a72ec1c9-ba30-422f-9b11-fcc7fd269e33, Initiating User: 0c089e94-0b42-e911-a878-000d3a6a0a90
Initialise Sales Manager 
Call to SalesManager to create Integration Request 
Retrieving Salesman (CONTACT) for "" [689825e6-1b90-e966-a882-000d3a6a065c]
Retrieving all Sales Codes for Salesman
Total number of Sales Enteries: 1
Retrieving ERP Data for Cales Code "East" [8180hh6c-2ac8-e711-a825-000d3ae0a7f8]
Data Area = "888"
WARNING: Region does not specify Regional Manager
Retrieving Salesman Profile "Jay Thompson" [d411a654-1c90-e911-a882-000d3a6a065c]
Invalid Plugin Execution exception detected: An item with the same sames code has already been added.



Error Message:

Unhandled exception: 
Exception type: Microsoft.Xrm.Sdk.InvalidPluginExecutionException
Message: An item with the same sames code has already been added.

Anyway, below is the simple method I wrote to format this, so that clean string is passed. This is so simple but time someone need to spend on this could be really costly. So thought of sharing.

public static string CleanErrorMsg(string valString)
{
var strWithSpaces = valString.Replace("\" ", " ").Replace(Environment.NewLine, " ").Replace(",", " ").Replace("\r\n", " ").Trim();
return Regex.Replace(strWithSpaces, @"\s+", " ");
}

This really removes below;
- troublesome characters
- Line breakers
- Extra spaces (more than one space together)

Jun 8, 2019

Client side operations using Xrm.WebApi

Xrm.WebApi provides properties and methods to use Dynamics 365 Customer Engagement Web API. Here I am presenting some code snippets.

function XrmWebApi_Create()
{
        var data = {  
        "su_name": "Test2 Office",
        "su_ContactId@odata.bind": "/contacts(dba3e5b9-88df-e311-b8e5-6c3be5a8b200)",
        "su_type": 100000001
        }  
 
        Xrm.WebApi.createRecord("su_office", data).then(  
        function success(result) {  
                 alert("Success. Id: " + result.id);  
        },  
        function(error) {  
                alert("Error: " + error.message);  
        }  
        );
}

function XrmWebApi_Update()
{
        var data = {
        "su_name": "Test2 Office NEW",
        "su_type": 100000002
        }

        Xrm.WebApi.updateRecord("su_office", "c1730963-b087-e911-a992-00224800cc7a", data).then(
        function success(result) {
                alert("Updated");
        },
        function (error) {
                console.log(error.message);
        }
        );
}


function XrmWebApi_Retrive()
{
        parent.Xrm.WebApi.retrieveRecord("su_office", "c1730963-b087-e911-a992-00224800cc7a", "?$select=su_name,su_type").then(
        function success(result) {
                alert("Retrieved values: Name:" + result.su_name + " Type:" + result.su_type);
        },
        function (error) {
                alert(error.message);
        }
        );
}

function XrmWebApi_RetriveMultiple()
{
        Xrm.WebApi.retrieveMultipleRecords("su_office", "?$select=su_name&$filter=su_type eq 100000002").then(
        function success(result) {
                for (var i = 0; i < result.entities.length; i++) {
                     alert(result.entities[i].su_name);
                     break; 
                }
        },
        function(error) {  
                        alert("Error: " + error.message);  
        }  
        );
}

function XrmWebApi_Delete()
{
        Xrm.WebApi.deleteRecord("su_office", "fca4b45e-4087-e911-a992-00224800cc7a").then(
        function success(result) {
                alert("Deleted");
        },
        function (error) {
                alert(error.message);
        }
        );
}

Sometimes it can be quite difficult to deal with PartyLists (in Activities). So I am giving a code snippet for that. This example is for creating a Phone Call. Please note how below fields are being set;

a. Regarding Object
b. ParttList

function XrmWebApi_Create()
{
        var contact = "/contacts(dba3e5b9-88df-e311-b8e5-6c3be5a8b200)";
        var user = "/systemusers(63265A2B-F31C-4E0E-951D-13974E3CCB4E)";
        var activityParties = new Array();
        var partyFrom = {"partyid_contact@odata.bind" : contact, "participationtypemask" : 1};
        var partyTo = {"partyid_systemuser@odata.bind" : user, "participationtypemask" : 2};
        activityParties[0] = partyFrom;
        activityParties[1] = partyTo;

        var data = {  
        "subject": "0003",        
        "phonenumber": "789999",
        "description": "test",
        "regardingobjectid_account@odata.bind": "/accounts(aaa19cdd-88df-e311-b8e5-6c3be5a8b200)",
        "phonecall_activity_parties":activityParties
        }  
 
        Xrm.WebApi.createRecord("phonecall", data).then(  
        function success(result) {  
                 alert("Success. Id: " + result.id);  
        },  
        function(error) {  
                alert("Error: " + error.message);  
        }  
        );
}

Please refer this on passing FetchXml to WebApi.

Enjoy!

Apr 29, 2019

SSIS Connections cannot be changed

This a quite common issue you would come across, especially if you want to change the Data Source  of a SSIS Package and check some functionality against a different data source. Issue is, how many times you change it from Connection Manager (ex. Server Name), it jumps back to the previous value.

Reason behind this behavior is these details are associated with Parameters and sometimes it may be not present in the view.

You can right click the Project.params and view the code in such cases as shown below;


Now inspect the code;


You will realize that previous details (ex. Server) are set as shown above and they doesn't get change when you change the Connection Manager from UI. So you need to change this file with new details. This fix the issue.

Apr 12, 2019

Dynamics 365 App for Outlook – How to setup?


When we need to use Dynamics 365 together with our familiar outlook email client, actually we have two options as Dynamics 365 Client for Outlook and Dynamics 365 App for Outlook.

Dynamics 365 Client for Outlook is actually the old method, which require manually installing the client in all machines. This supports off-line capabilities helping user in the field without internet connectivity. This gives all the necessary functionalities allowing users to adopt to Dynamics functions within the outlook but doesn’t work in mobile or web versions. Still biggest drawback is it pushes outlook to perform slow. Anyway, Microsoft has announced its deprecation.

Dynamics 365 App for Outlook is the successor. This supports all Outlook versions such as full client, web and mobile. No manual installation is required since D365 itself can be configured to push the add-in to Outlook. Since this uses Dynamics Web API to retrieve real data this App is very fast. Anyway, Server-side synchronization with Exchange is required.

Let’s check how to set-up Dynamics 365 App for Outlook and tracking records;

1) Configure exchange as explained here.

2) Configure Dynamics 365 CE

Browse to Apps for Dynamics 365 by clicking settings button.


You will see below;


Once click Microsoft Dynamics 365 App for Outlook, you should see below notice if everything is in order.


3) Dynamics 365 CE functionality in Outlook

Now you will see Dynamics 365 icon in right hand corner of the Outlook as below;


Once you select e-mails, relevant Dynamics 365 data will be loaded in new pane appear in right hand-side

a. Email from a Contact


b. Email from a User


c. Email from unknown person


Now it is allowed to create a Contact or Account using this record.


Showing auto-populated fields prior to saving the record.


d. Set regarding record - Click the three dots below the Set Regarding label to associate any D365 record to this email


e. Track Record – Click three dots after Not Tracked label to track the record in D365.




f. New Email – It is possible to Track and Set Regarding for new emails as well


Some Notes on Tracking and Set Regarding;

If Track without setting the Set Regarding for a email (from known/ unknown party), it will create Email in D365 without a regarding object. This is not really useful since record will not be associated to anything. Still you can reply to this email from D365. So it is always encouraged setting the Set Regarding.

Also when Tracking emails from unknown party it's also encouraged to create a Contact (or lead etc.)

Anyway, if you Track an email in D365 from unknown party without creating a record (Contact, Lead etc.) it still allows to reply from D365. (** This is a special case where you can send a email to someone who is not a Contact, Lead , etc.)

Refer this article for complete guide on using Dynamics 365 App for Outlook.

Apr 4, 2019

Enabling Multi-Factor Authentication (MFA) for Microsoft users

Multi-factor authentication (aka MFA) is a new necessity in ever growing need for secure business systems. No exception for Dynamics 365. It is now possible for Dynamics since Microsoft users are easily enabled for MFA. In fact, any Microsoft application, including Dynamics products, are ready with MFA.

Let’s see the easy steps to enable MFA.

1.Enable users for MFA

Login to Office Admin center and click Active Users. Then click More> Multifactor Authentication Setup.


Resulting window will list down all the active users as below. Then you are allowed to pick the users those need MFA to be enabled and click Enable link.


Then you will see below general confirmation pop-up.


Upon confirmation, you will be notified on success of operation.


2. First time login

When user log to the system for the first time, below form will be presented to grab the phone number details.


One filled and submit, code will be sent to the given mobile for verification.


Once verify with the code, resulting pop-up will produce one important piece of information which is the app password. This is the password user is required to use for the situations mobile authentication is not possible such as Integrations and different apps (Outlook etc.).

3. Subsequent logins

Now user is MFA enabled. Each time user is sent a code to the mobile and requested to provide it prior to successful login.

Mar 31, 2019

Microsoft Cognitive Services – Speech recognize

Microsoft Cognitive services allow you using many advance Machine Learning driven services easily. This offering consists of Visual identification, Speech identification etc. These services typically should be gained as an Azure service. Anyway, here we try Speech recognition API using a trial subscription (no Azure needed).

1. Get the trial subscription

a. Browse to Cognitive Service trial link here.

b. Select Speech API tab

c. Click Get API Key and click Next while region is selected (Default: United States)

d. Resulting page would show relevant API endpoint and keys.

2. Sample Console Application to test

a. Start Visual Studio 2017

b. It is required to have .NET cross-platform development workload is available.
To check go to Tools > Get Tools and Features



c. Open a New Console Application


d. Now install Speech NuGet Package


     Search for Microsoft.CognitiveServices.Speech and install the resulting Package.


e. Add the code as below;

using System;
using System.Threading.Tasks;
using Microsoft.CognitiveServices.Speech;

namespace CognitiveSrvSpeech1
{
    class Program
    {
        static void Main()
        {
            RecognizeSpeechAsync().Wait();
            Console.WriteLine("Please press a key to continue.");
            Console.ReadLine();
        }

        public static async Task RecognizeSpeechAsync()
        {
            var config = SpeechConfig.FromSubscription("<Subscription Key>", "<Region>");
            // Creates a speech recognizer.
            using (var recognizer = new SpeechRecognizer(config))
            {
                Console.WriteLine("Please say the text you need to convert...");
                var result = await recognizer.RecognizeOnceAsync();
                // Checks result.
                if (result.Reason == ResultReason.RecognizedSpeech)
                {
                    Console.WriteLine($"We recognized: {result.Text}");
                }
                else if (result.Reason == ResultReason.NoMatch)
                {
                    Console.WriteLine($"NOMATCH: Speech could not be recognized.");
                }
                else if (result.Reason == ResultReason.Canceled)
                {
                    var cancellation = CancellationDetails.FromResult(result);
                    Console.WriteLine($"CANCELED: Reason={cancellation.Reason}");

                    if (cancellation.Reason == CancellationReason.Error)
                    {
                        Console.WriteLine($"CANCELED: ErrorCode={cancellation.ErrorCode}");
                        Console.WriteLine($"CANCELED: ErrorDetails={cancellation.ErrorDetails}");
                        Console.WriteLine($"CANCELED: Did you update the subscription info?");
                    }
                }
            }
        }
    }
}

Make sure you replace the <Subscription Key> and <Region> with the values you obtained in 1.d step. Region is westus, if you left default United States region when getting the trial subscription.

f. Run the application and see how what you say is converted to text. Enjoy!


Ref: https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/quickstart-csharp-dotnetcore-windows