Sunday, April 10, 2016

Key values in CRM Lookup fields

I have been working with CRM for nearly 9 years but I have never been aware of the “Key Values” available in the lookup control. Generally, when we retrieve the lookup control in JavaScript, we use code similar to the following :
 lookupFieldObject = Xrm.Page.data.entity.attributes.get('parentaccountid');   
 if (lookupFieldObject.getValue() != null) {   
 entityId = lookupFieldObject.getValue()[0].id;   
 entityName = lookupFieldObject.getValue()[0].entityType;   
 entityLabel = lookupFieldObject.getValue()[0].name;   
 }   
 What are the key values for lookup control?
If you select the lookup entity on a form, the lookup control also contains a collection of key values that have other fields from the lookup entity. The screenshot below displays the key values available for an account lookup, when I select a parent account from a contact form.
 
The screenshot displays the collection of other account attributes such as abn, address1state, address1_city, primarycontactid, telephone 1, etc. I did not know that these values were also available for a lookup control. Sometimes we need to retrieve the other attributes of the lookup entity and these key values can be used to retrieve them, instead of making an additional call.
Where are these values coming from?
After doing a bit of research, we have found that these attributes/values are coming from the lookup view of the entity.
Here is the lookup view of the account entity in my system.
These are the same fields as shown in the Key Values collection in the screen shot above.
 
Gotchas
Now to the bad news. These key values are not available all the time. These values are only available if you select the value manually from the form. These values won’t be available if the lookup value is set via code or a workflow. My colleague, Mr. Davey has an explanation, that these values get cached when we select the lookup control on the form. That sounds like a good explanation.
 

Tuesday, April 5, 2016

A bit more about SDK “Merge” message in CRM plugins

One of our clients is using auto numbering on contact records. A few days ago, they reported to have duplicates in the system. So I started the investigation, I could not believe that this could happen in our solution. But what I had found out was that those duplicates were generated as a part of the "Merge" functionality available in CRM. Every time users "Merge" the customer and choose the subordinate's customer number to come across, it will override the master record's customer number with the subordinate record's number.

So we decided to write a plugin that will stop the users from overriding the master record's customer number.
The  context.InputParameters  collection of the merge message contains 3 of the following objects
  1. Target ( entity reference of the master record)
  2. SubordinateId ( guid of the subordinate record)
  3. UpdateContent ( temporary entity that contains the attribute that will be passed to the master record).

The following example does not use the SubordinateId object. We are only using Target and UpdateContent entity. Like any other plugin entity, the "UpdateContent" entity only contains attributes that will be updated in the master record.
Code
 protected void ExecutePreCustomerMerge(LocalPluginContext localContext)  
  {  
       if (localContext == null)  
       {  
         throw new ArgumentNullException("localContext");  
       }  
       IPluginExecutionContext context = localContext.PluginExecutionContext;  
       ITracingService trace = localContext.TracingService;  
       IOrganizationService service = localContext.OrganizationService;  
       // if the sdk message is merge  
       if (context.MessageName.Equals("merge", StringComparison.InvariantCultureIgnoreCase))  
       {  
         //get the merged entity  
         Entity updateContentData = context.InputParameters["UpdateContent"] as Entity;  
         //if the merged record is null then do nothing  
         if (updateContentData == null) { return; }  
           //get the customer number of merged record  
           string mergedCustNumber = updateContentData.GetAttributeValue<string>("custNumber");  
           if (mergedCustNumber == null) return;  
           else  
           {  
             // if the merged CustNumber is not null retrieve the CustNumber of the master record to display in the message  
             // get the master entity reference  
             EntityReference targetReference = (EntityReference)context.InputParameters["Target"];  
             //trace.Trace("entity reference " + targetReference.LogicalName.ToString());  
             //retrieve the CustNumber number of the master entity  
             Entity target = service.Retrieve(targetReference.LogicalName, targetReference.Id, new ColumnSet("custNumber"));  
             string masterCustNumber = target.GetAttributeValue<string>("custNumber");  
             throw new InvalidPluginExecutionException("You cannot update the CustNumber of the Master record. Master CustNumber :" + masterCustNumber + " and New CustNumber :" + mergedCustNumber);  
           }  
         }  
  }  

Some Other Observations
When I tested this plugin in CRM2013 roll up 2, I did not receive the message thrown using InvalidPuginExecutionException. Instead, I received the following message:
I think it is a bug in CRM2013, that is now fixed in CRM2013 SP1.

When I tested the same plugin on CRM2013 SP1 UR3, It displays the proper message, as thrown by the InvalidPuginExecutionException.




Thursday, February 4, 2016

Keypress methods in CRM2016

CRM2016 introduced the new keypress methods for text and numeric fields to support keypress events on CRM forms. These methods will be useful to provide immediate feedback on key press. Traditionally, all the validations and actions were executed via the “on Change” event. These methods include
  • addOnKeyPress
Use this method to attach an event handler to the keypress event
  • removeOnKeyPress
Use this method to remove an event handler to the keypress event
  • removeOnKeyPress
Use this method to manually fire an event handler to the keypress event
For this blog, I have created sample code that will stop the input of numbers in the name field of the account record.
Here are the steps
  1. Create a new JScript web resource and add the following code to it.

     // JavaScript source code  
     function keyPress()  
     {  
       //attach the validateInput function to the keypress event of the name attribute  
       Xrm.Page.getControl("name").addOnKeyPress(validateInput);    
     }  
     function validateInput() {  
       //get the value of the name field  
       var input = Xrm.Page.getControl("name").getValue();  
       if (input != "") {  
         //check if the last key pressed is a number  
         if (isNaN(input.substr(input.length - 1)) == false) {  
           //display the message  
           alert("Numeric values are not allowed in the account name");  
         }  
       }  
     }  
    

  2. Save and publish the web resource.
  3. Open the account form in customization mode.
  4. Add the JScript web resource to the form and call the keypress() method from load.
  5. Save and publish the account form.
  6. Test the code. If you try to press a number key on the name field you will get an error message as shown in the following screenshot.


Sunday, January 31, 2016

CRM Web API and CORS Support

With the introduction of WEB APIs in CRM2016, you can do all the things that you used to do using organization services (SOAP endpoints). It is good news but the greatest news for me is the addition of CORS support.


Remember the days when you could not use CRM REST endpoints for standalone applications because of CORS, now you can. According to the SDK, the new WEB API supports cross-origin web resource request when called using OAuth token authentication.This means that we can now create standalone and mobile applications using WEB API. The SDK contains a sample page to demonstrate this functionality. In this blog, I will follow that sample with some additional screenshots.


Here is the step by step guide to create a html page using CRM Web API. I am using CRM Online for this post.

Signup up CRM2016 online

Setup a 30 day free trial CRM2016 organization from the following link:

Creating the web application

  1. Using Visual Studio 2015, create a new ASP.Net web application project.
  2. Choose the “Empty” template.
  3. Add a new HTML page to the project and name it “CreateAccounts”. Copy the following code and paste it into the HTML page.

     <!DOCTYPE html>  
     <html>  
     <head>  
       <title>Simple SPA</title>  
       <meta charset="utf-8" />  
       <script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.min.js"></script>  
       <script type="text/javascript">  
      "use strict";  
      //Set these variables to match your environment  
                    var organizationURI = "https://hpeaustralia.crm6.dynamics.com"; //The URL to connect to CRM Online  
      var tenant = "hpeaustralia.onmicrosoft.com"; //The name of the Azure AD organization you use  
      var clientId = "ef9166ae-8d3f-48c9-b260-243d29f15355"; //The ClientId you got when you registered the application  
      var pageUrl = "http://localhost:39812/CRMAccounts.html"; //The URL of this page in your development environment when debugging.  
      var user, authContext, message, errorMessage, loginButton, logoutButton, getAccountsButton, accountsTable, accountsTableBody;  
      //Configuration data for AuthenticationContext  
      var endpoints = {  
       orgUri: organizationURI  
      };  
      window.config = {  
       tenant: tenant,  
       clientId: clientId,  
       postLogoutRedirectUri: pageUrl,  
       endpoints: endpoints,  
       cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.  
      };  
      document.onreadystatechange = function () {  
       if (document.readyState == "complete") {  
       //Set DOM elements referenced by scripts  
       message = document.getElementById("message");  
       errorMessage = document.getElementById("errorMessage");  
       loginButton = document.getElementById("login");  
       logoutButton = document.getElementById("logout");  
       getAccountsButton = document.getElementById("getAccounts");  
       accountsTable = document.getElementById("accountsTable");  
       accountsTableBody = document.getElementById("accountsTableBody");  
       //Event handlers on DOM elements  
       loginButton.addEventListener("click", login);  
       logoutButton.addEventListener("click", logout);  
       getAccountsButton.addEventListener("click", getAccounts);  
       //call authentication function  
       authenticate();  
       if (user) {  
        loginButton.style.display = "none";  
        logoutButton.style.display = "block";  
        getAccountsButton.style.display = "block";  
        var helloMessage = document.createElement("p");  
        helloMessage.textContent = "Hello " + user.profile.name;  
        message.appendChild(helloMessage)  
       }  
       else {  
        loginButton.style.display = "block";  
        logoutButton.style.display = "none";  
        getAccountsButton.style.display = "none";  
       }  
       }  
      }  
      // Function that manages authentication  
      function authenticate() {  
       //OAuth context  
       authContext = new AuthenticationContext(config);  
       // Check For & Handle Redirect From AAD After Login  
       var isCallback = authContext.isCallback(window.location.hash);  
       if (isCallback) {  
       authContext.handleWindowCallback();  
       }  
       var loginError = authContext.getLoginError();  
       if (isCallback && !loginError) {  
       window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);  
       }  
       else {  
       errorMessage.textContent = loginError;  
       }  
       user = authContext.getCachedUser();  
      }  
      //function that logs in the user  
      function login() {  
       authContext.login();  
      }  
      //function that logs out the user  
      function logout() {  
       authContext.logOut();  
       accountsTable.style.display = "none";  
       accountsTableBody.innerHTML = "";  
      }  
     //function that initiates retrieval of accounts  
      function getAccounts() {  
       getAccountsButton.disabled = true;  
       var retrievingAccountsMessage = document.createElement("p");  
       retrievingAccountsMessage.textContent = "Retrieving 10 accounts from " + organizationURI + "/api/data/v8.0/accounts";  
       message.appendChild(retrievingAccountsMessage)  
       // Function to perform operation is passed as a parameter to the aquireToken method  
       authContext.acquireToken(organizationURI, retrieveAccounts)  
      }  
     //Function that actually retrieves the accounts  
      function retrieveAccounts(error, token) {  
       // Handle ADAL Errors.  
       if (error || !token) {  
       errorMessage.textContent = 'ADAL error occurred: ' + error;  
       return;  
       }  
       var req = new XMLHttpRequest()  
       req.open("GET", encodeURI(organizationURI + "/api/data/v8.0/accounts?$select=name,address1_city&$top=10"), true);  
       //Set Bearer token  
       req.setRequestHeader("Authorization", "Bearer " + token);  
       req.setRequestHeader("Accept", "application/json");  
       req.setRequestHeader("Content-Type", "application/json; charset=utf-8");  
       req.setRequestHeader("OData-MaxVersion", "4.0");  
       req.setRequestHeader("OData-Version", "4.0");  
       req.onreadystatechange = function () {  
       if (this.readyState == 4 /* complete */) {  
        req.onreadystatechange = null;  
        if (this.status == 200) {  
        var accounts = JSON.parse(this.response).value;  
        renderAccounts(accounts);  
        }  
        else {  
        var error = JSON.parse(this.response).error;  
        console.log(error.message);  
        errorMessage.textContent = error.message;  
        }  
       }  
       };  
       req.send();  
      }  
      //Function that writes account data to the accountsTable  
      function renderAccounts(accounts) {  
       accounts.forEach(function (account) {  
       var name = account.name;  
       var city = account.address1_city;  
       var nameCell = document.createElement("td");  
       nameCell.textContent = name;  
       var cityCell = document.createElement("td");  
       cityCell.textContent = city;  
       var row = document.createElement("tr");  
       row.appendChild(nameCell);  
       row.appendChild(cityCell);  
       accountsTableBody.appendChild(row);  
       });  
       accountsTable.style.display = "block";  
      }  
       </script>  
       <style>  
         body {  
           font-family: 'Segoe UI';  
         }  
         table {  
           border-collapse: collapse;  
         }  
         td, th {  
           border: 1px solid black;  
         }  
         #errorMessage {  
           color: red;  
         }  
         #message {  
           color: green;  
         }  
       </style>  
     </head>  
     <body>  
       <button id="login">Login</button>  
       <button id="logout" style="display:none;">Logout</button>  
       <button id="getAccounts" style="display:none;">Get Accounts</button>  
       <div id="errorMessage"></div>  
       <div id="message"></div>  
       <table id="accountsTable" style="display:none;">  
         <thead><tr><th>Name</th><th>City</th></tr></thead>  
         <tbody id="accountsTableBody"></tbody>  
       </table>  
     </body>  
     </html>  
    
  4. The highlighted part in the above code are the variables that needs to be set to make this code work properly. I have left my settings for those variables in the code. Please change the values of these variables to match your system.
  5. Run the code. It will open a new browser window. Check the URL of that page. It will look something like http://localhost:3764/CrerateAccounts.html
  6. Replace the value of the variable “pageUrl” with the your URL from step 5 above.
Note:  The next heading “Register the application with windows Azure” will provide the value for the “clientId” variable of the code.

Register the application with windows Azure

  1. If you don’t have a Windows Azure account then you can create a trial account attached to CRM Online domain by logging into Office 365. Go to step 10.
  2. If you already have a Windows Azure subscription then you need to associate your Office 365 subscription (CRM online account) to the Windows Azure subscription. I already have an Window Azure subscription.
  3. Logon to Azure portal account.
  4. Click on “Active Directory” as shown in the following screenshot and click “New” to associate your Office 365 domain to Windows Azure.
  5. Select “Custom Create” from the following screen
  6. Select “Use existing directory” and tick “I am ready to be signed out”.
  7. Login as the admin of your Office 365 (CRM Online) account. You will see the following message.
  8. Click “Sign out now” on the following screen
  9. Login to the Windows Azure portal again using your Windows Azure account. You will now see 2 active directories.
  10. Click on the CRM directory, It will display the following screen.
  11. Click on the "Applications" tab and select add, as highlighted in the following screenshot.
  12. Select “Add an application my organization is developing”.


  13. Enter the following information:
    • Sign-on URL
      This is the URL which the user should be redirected to after they sign in. For debugging purposes in Visual Studio it should be http://localhost:####/CRMAccounts.html where #### represents the port number you got from step 4 of the Create a web application project procedure.  These are the same values as step 5 of the “Creating the web application”  section of the blog.
    • APP ID URI
      This must be a unique identifier for the application. Use https://XXXX.onmicrosoft.com/CRMAccounts . Where  XXXX is the Active Directory tenant.
  14. With the tab of the newly registered app selected, click "Configure" and locate the Client id. Copy the id and update the “clientId”  variable  of the web application.
  15. Select “Add application” and choose “Dynamics CRM Online”.
  16. Under permissions to other applications, you will find a row for "Dynamics CRM Online" and "Delegated Permissions: 0". Select this and add "Access CRM Online as organization users".
  17. Save the application registration.
  18. Click on “Manage Manifest” and select “Download Manifest”.
  19. Manifest is a json file. Open the file and locate the line: "oauth2AllowImplicitFlow": false, and change false to true and save the file.
  20. Upload the manifest file.


Run the web application

  1. Now go back to your web application and press F5 to run the page. It will open the page in the browser.
  2. Click on login. The system will take you to the login page. The page will display the name of the logged in user.
  3. Click on “Get Accounts”. The browser will display the following page.

That is it. Happy coding..

Sunday, January 24, 2016

How to fix the date format in CRM Notes

A few months ago, one of our customers reported that CRM was displaying an incorrectly formatted date on the notes. The issue was that the “Created On/ Modified On” date on notes was showing American date format (mm/dd/yyyy).
 
The CRM "System Settings" is configured to use “Australian English” format and so does the user options including the time zone. We are working with CRM2013 (on-premise).
 
The following screen shot is displaying the record and a note attached to that record. These records were created on the same date.
 
The “Created On” date is displaying the correct date 12th of March 2015 but the notes are showing “Thursday, 3 December 2015”.
 
I don’t know the reason behind this problem but I found a solution on this forum.
 

Solution

  1. Logon to the CRM server. Open the IIS Manager.
  2. Navigate to “Microsoft Dynamics CRM” site and click on “.Net Globalization”. IIS Manager will display the following screen.

  3. Change the culture value to “English (United States)”
  4. Apply the change and restart the website.
  5. Open the record and check the dates on the Notes. It should display the right date now.

Monday, December 21, 2015

CRM OData (REST) queries and special characters

In this blog, we will discuss "How to handle special characters in CRM OData queries". Every now an then we have to query the data based on the fields other than entity Ids. Most of the time OData queries works as expected except if the filtering parameter conatins special characters. OData queries does not support special characters as a filter parameters.

For example, if you are searching for a customer with an apostrophe in the  name (“Simon O’ Donnel”),  the OData query will throw a following error message.












If you try to search for a customers with special characters in the email address (abc#123@test.com) , we  encountered the following error message.











The reason is that these special characters has different meaning when used in URLs. The JavaScript “encodeUri” or  “encodeUriComponent” does not solve this problem. Here is the list of the special characters that needs to be replaced when used in the OData queries.
https://msdn.microsoft.com/en-us/library/aa226544(SQL.80).aspx

Special character Special meaning Hexadecimal value
+ Indicates a space (spaces cannot be used in a URL). %2B
/ Separates directories and subdirectories. %2F
? Separates the actual URL and the parameters. %3F
% Specifies special characters. %25
# Indicates bookmarks. %23
& Separator between parameters specified in the URL. %26

The apostrophe is not mentioned in the table. The apostrophe needs to be replaced with double apostrophes.
Now you can write a separate function to replace these characters with the appropriate hexadecimal value.

Note: Do not use the “JavaScript String replace() Method”. It will replace the first occurrence of the special characters. if you  have 2 occurence of the same special characters in the filtering parameter, it will fail. So use the regular expression to replace the characters.

You can create a  separate function and call it for all the filtering parameter before you pass it to the query.

function replaceSpecialCharacters(attribute) {
  // replace the single quotes
     attribute = attribute.replace(/'/g, "''");

     attribute = attribute.replace(/"+"/g, "%2B");
     attribute = attribute.replace(/\//g, "%2F");
     attribute = attribute.replace(/"?"/g, "%3F");
     attribute = attribute.replace(/%/g, "%25");
     attribute = attribute.replace(/#/g, "%23");
     attribute = attribute.replace(/&/g, "%26");
     return attribute;
}


Happy Coding…

Friday, October 30, 2015

CRM SOAP Library- Part 3 (Testing CRM web resources for CRM Online in Visual Studio)

This is a part 3 of the CRM Soap Library series. This blog covers how to test the web resources that make CRM web services calls  from visual studio for MSCRM Online.

If you run the CRM soap library sample or "StartUp.htm" page from my last blog CRM SOAP Library - Part 2 (Project Structure) , you will get the following  error.

“Sdk.Util.getClientUrl Unable to get clientUrl. Context not available.”

This is a straight forward error. The library is trying to get the CRM URL from global context or Xrm.page.Context. For development purposes,  you can replace the “GetGlobalContext().getClientUrl()” line in “Sdk.Soap.min.js” with your  CRM Online URL.


Now if you try to run the code after making the change,  code will throw the following error.

“status: 401: Unauthorized”

It becomes a pain when you have to keep deploying Java Script resources to CRM to test them. There are hundreds of forums/blogs that talk about this problem but you can’t not see any solution.

I figured out a way around this problem with the help of “Fiddler”.So, here is hack that will allow you to run/test you web resources without deploying them in CRM.

Here is the step by step tutorial to achieve this.
  1. If you don't have fiddler installed already, install it.you can download it from http://www.telerik.com/download/fiddler.
  2. Start the fiddler and make sure “Capture Traffic” is selected  in the “File” menu as shown in the screen shot below.
     image
  3. Logon to your CRM organisation. I am using CRM Online for this tutorial.
  4. Switch to fiddler and double click on  a CRM URL from the session pane as shown in the screen shot.
    image
  5. In the second pane, select “Inspectors>>Cookies” as shown in the screen shot below. if the contents of cookies for the selected request does not look the same try some other URL from the session list.
    image
  6. Look for “MSISAuth” in the contents and select everything from that text onwards and paste it notepad.
  7. Select “Tools>>Customize Rules” as shown in the screen shot below. It will open the “CustomRules.js” file.
    image
  8. Search for  “OnBeforeRequest(oSession: Session) function in the file and add the following code to it.

    1:  if (oSession.PathAndQuery=="/XRMServices/2011/Organization.svc/web")  
    2:   {  
    3:               //appending the cookie data for the request  
    4:            oSession.oRequest["Cookie"] = (oSession.oRequest["Cookie"] + "; text copied in step 6" );  
    5:  }  
    
    

  9. Replace the highlighted text with the text copied in step 6. Save the changes to the file.
  10. Open up your HTML page in VS and test it. You won’t get the unauthorised error  anymore.