Basic CRUD using Xrm.WebApi

UPDATE (18/10): It appears Andrii got to this topic first -> http://butenko.pro/2017/10/05/microsoft-dynamics-365-v9-0-usage-of-new-oob-webapi-functions-part-1/. I should have probably subscribed to his RSS feed – could have saved some time for me. Anyway there is also a Part 2 that he has not posted yet, so I am looking forward to see what I missed.

Dynamics 365 Customer Engagement v9 has added CRUD functionality to query the WebAPI endpoint using Client API.

Xrm Web Api.png

Based on my initial analysis, this seems to be a work in progress and more functions will be added over time. This is some sample code how you can do the basic CRUD using this new feature. This is not an exhaustive documentation, but considering that there is nothing about this in the official documentation, it is a starting point.

Create : Method signature is ƒ (entityType, data)

Sample code to create 3 contact records

[...new Array(3).keys()].forEach(x => Xrm.WebApi.createRecord('contact', {
    firstname: 'Test',
    lastname: `Contact${x}`
}).then(c => console.log(`${x}: Contact with id ${c.id} created`))
  .fail(e => console.log(e.message)))

WebApi Create.png

Retrieve: Method signature is ƒ (entityName, entityId, options)

Sample code to retrieve contact record based on the primary key

Xrm.WebApi.retrieveRecord('contact', 'cadf8ac6-17b1-e711-a842-000d3ad11148', '$select=telephone1')
  .then(x => console.log(`Telephone: ${x.telephone1}`))
  .fail(e => console.log(e.message))

WebApi Retrieve

RetrieveMultiple: Method signature is f(entityType, options, maxPageSize)

Sample code to retrieve 10 contact records without any conditions.

Xrm.WebApi.retrieveMultipleRecords('contact', '$select=fullname,telephone1', 10)
  .then(x => x.entities.forEach(c => console.log(`Contact id: ${c.contactid}, fullname: ${c.fullname}, telephone1: ${c.telephone1}`)))
  .fail(e => console.log(e.message))

WebApi RetrieveMultiple.png

Update: Method signature is ƒ (entityName, entityId, data)

Sample code to update field on contact record

Xrm.WebApi.updateRecord('contact', 'cadf8ac6-17b1-e711-a842-000d3ad11148', {
    telephone1: '12345'
}).then(x => console.log(`Contact with id ${x.id} updated`))
  .fail(x => console.log(x.message))<span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>

WebApi Update.png

Delete: Method signature is ƒ (entityName, entityId)

Xrm.WebApi.deleteRecord('contact', '88E682D8-18B1-E711-A842-000D3AD11148')
  .then(c => console.log('Contact deleted'))
  .fail(x => console.log(x.message))

WebApi Delete.png

What is not yet done/appears to be in progress

  1. Xrm.WebApi.offline not yet implemented
  2. Ability to construct custom OData requests to pass into Xrm.WebApi.execute
  3. Batching multiple requests

You can use this on your client side code on v9. It is quite basic at the moment, but you don’t need to include any external libraries. But in more advanced scenarios, you can always use Xrm WebAPI Client till these features are made available in the Client API.

Referencehttps://docs.microsoft.com/en-au/dynamics365/get-started/whats-new/customer-engagement/new-in-july-2017-update-for-developers#new-client-apis

Advertisements

Cancelling save event based on the result of async operation

When you want to cancel a save event in CRM Dynamics 365 Customer Engagement, you use “preventDefault()”  to block the save operation. This works when you block the operation based on the information that is currently on the form/page, but it does not work, if you want to block the save based on the result of an async operation.

In this contrived example, I would like to block the save of the current form, if there exists an user with “homephone” field set to 12345. The async operation is performed by “retrieveMultipleRecords” which returns a Promise.

The code below does not work

Xrm.Page.data.entity.addOnSave((e)=>{
	Xrm.WebApi.retrieveMultipleRecords('systemuser','$select=fullname,jobtitle,homephone').then(x=>{
		console.log(`DataXml OnSave: ${Xrm.Page.data.entity.getDataXml()}`);
		if(x.entities.some(x=>x.homephone == '12345')){
			e.getEventArgs().preventDefault();
			console.log('User with homephone 12345 exists. Save blocked.');
		}
	});			
});

Result

Notice the the save event completed and form’s load event fired even though preventDefault ran. The “jobtitle” field that I modified also succeeded, when I expected it to not succeed.

Async Save block does not work

In order to block the save, you’ll have to restructure the code little differently, like the one below. Block save before async operation and explicitly call save, when your criteria for save is met and use closure variable to keep track of whether to save or not.

Working code

Xrm.Page.data.entity.addOnSave((()=>{
	let isSave = false;
	return (e)=>{
		console.log(`DataXml OnSave: ${Xrm.Page.data.entity.getDataXml()}`);
		if(isSave) {
			console.log('proceed to save');
			return;
		}
		else{
			e.getEventArgs().preventDefault();
			console.log('save blocked');			
			Xrm.WebApi.retrieveMultipleRecords('systemuser','$select=fullname,jobtitle,homephone').then(x=>{
				isSave = !x.entities.some(x=>x.homephone == '12345');
				if(isSave){
					Xrm.Page.data.save();
				}
				else{
					console.log('User with homephone 12345 exists. Save blocked.');
				}
			});			
		}
	}
})());

Result

Async Save block works.png

I have tested this only in Chrome on Dynamics 365 Online v9. Hope this is useful.

Puppeteer and Dynamics 365

Puppeteer is a Node API to drive Headless Chrome. I have used Selenium and DalekJS in the past to do some UI testing. I have been experimenting/learning puppeteer for the past few weeks and have found it to relatively easy to learn and use. It is still on alpha though and so there are some bugs.

In my sample repo (https://github.com/rajyraman/Puppeteer-Dynamics-365), I demonstrate:

  1. How to use puppeteer to login to ADFS OnPrem CRM
  2. How to use puppeteer to take full page screenshot
  3. Annotate the screenshot using imagemagick

I envision this repo to provide documentation assistance by capturing and annotating screenshots. Below are the steps to run this project:

    1. After cloning the github repo run the following command to download the npm packages: yarn
    2. Install imagemagick from https://www.imagemagick.org/script/download.php#windows
    3. Confirm that the path to magick.exe exists in PATHImagemagick path.png
    4. Create a new .env file in the root of the repo. Below is the .env file that I used: OnPrem
      env onprem
      Onlineenv online.png
    1. Change the USER_SELECTOR, PASSWORD_SELECTOR, LOGIN_SUBMIT_SELECTOR if they are different. These were the ids in the OnPrem ADFS login page
    2. Check the runsheet.csv file provided in the repo and change it to suit your screenshot requirements. The run sheet specifies the sequence of clicks. In this file, on line 2, I am specifying that I should first click Workspace group and then Clients subgroup. The screenshot should be annotated with text “Clients list”. On line 3, I am specifying that the “NEW” button should be clicked and the screenshot should be annotated as “New client form” and the file name should be “New Client Form.png”. The command bar clicks are always specified in a new line with blank group and subgroup.run sheet.png
    3. Run the node application using “node index.js”Run application.png

 

The screenshots will be captured with headless Chrome and annotated using imagemagick. Here is a sample screenshot:

Administration-Annotated.png

Possible future improvements:

  1. Build the exe using pkg and distribute the exe, .env and runsheet.csv. Building the exe using pkg requires a copy of the puppeteer folder from node_modules along side the exe
  2. Navigate to a record based on id
  3. Run workflow/dialogs
  4. Populate new entity form with data before command bar button click
  5. Automatically scroll if group is outside of viewport

Please submit your feedback/ideas/criticism on the comments area or as a issue in the repo.

Using Chrome DevTools to debug Bot Framework

When debugging the Bot Framework it is trivial to debug if you are developing in C#. It was bit of pain for me to do the same is node, as JavaScript is not my everyday language. But, after researching googling about this, I can say that it is really easy in node as well.

Below are the version details that I tried this on:

  • node: 8.1.2
  • npm: 5.0.3
  • OS: Windows 10
  • Chrome: Canary 61.0.3137.0
  • Bot Framework Emulator: 3.5.29

Below is my package.json for a simple “Hello World” bot.

{
  "name": "echo-bot",
  "version": "0.1.0",
  "license": "MIT",
  "description": "Echo bot example",
  "scripts": {
    "start": "node --inspect app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "botbuilder": "^3.8.4",
    "dotenv": "^4.0.0",
    "restify": "^4.3.0"
  }
}

Since I am developing this in Visual Studio Code, this is my tasks.json

{
    // Use IntelliSense to learn about possible Node.js debug attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "cwd": "${workspaceRoot}",
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "start"
            ],
            "name": "Launch Program"
        }
    ]
}

Now, to run the bot application in VSCode using Ctrl+F5 or typing “npm start” in the command prompt.

To debug the bot in Chrome DevTools, type “chrome://inspect/#devices“. Then click the “Open dedicated DevTools for Node” link. The page should be like this once you do this.

Chrome DevTools to debug Bot Framework.png

Chrome will now display the node application that it can detect in the Remote Target area. Click in the inspect link to open the DevTools. Now, set your breakpoint and use the emulator to send a message.

Debug botframework source

In the screenshot above you can see that I am already debugging using Chrome DevTools and debugger is waiting in the breakpoint that I set.

Reference:

https://nodejs.org/en/docs/inspector/

 

 

 

 

 

 

 

Troubleshooting Business Rules

Business Rules is composed of two components: a client side JavaScript and a server side workflow. You can use these queries to find out the details about this:

LinqPad – Query and result

business-rule-linqpad

FetchXml Builder – Query and result

fetchxml-query

fetchxml-result

One gotcha with the business rule is that its behavior will deviate, if a field that is required by the business rule is removed on the form. I will demonstrate this, with the business rule below.

contracting-unit-is-required

This business rule sets the “Contracting Unit” to required, if Order Type is “Work Based”. Below are the screenshots of the form in two different scenarios:

With “Order Type” present on the form

with-order-type

Without “Order Type” present on the form

Without Order Type.png

As you can see, “Contracting Unit” is set to required, only if the “Order Type” field is present on the form, even if the value of “Order Type” is “Work based”. At present, there seems to no check in the form customisation area to prevent a field from being removed, if is required by a business rule. This is how the “Contracting Unit is required” business rule, gets translated into JavaScript.

business-rule-chrome-devtools

When you debug this using Chrome Dev Tools, you can easily see why the “Contracting Unit” field is not being set to required.

business-rule-chrome-devtools-debug

To assist developers who are troubleshooting why a business rule is not working, I have developed this simple script to run in the DevTools console, that lists the fields that are required by the Business Rules, but are not present in the form. This has to be run in the context of ClientApiWrapper IFrame.

let formAttribs = Xrm.Page.getAttribute().map(a=>a.getName()); Object.keys(Mscrm.BusinessRulesScript.AttributesOnChangeHandlers).filter(x=>!formAttribs.includes(x))

Here is a sample output of this script.

business-rule-field-dependencies

It is saying that “Order Type” should be present in the form, as it is required by a Business Rule that is running on the form. It uses an unsupported internal method to identify this information, and so I recommend that it be used in devtools console only.

Further Reading:

Understanding Process Triggers and Business Rule internals

Bug: Notes creation in custom entity

There currently seems to be an issue in Dynamics 365 Online, where you can see notes in area in the custom entity, but cannot create new notes. I could reproduce this issue in this version -> Version 1612 (8.2.0.773) (DB 8.2.0.764)

This is how the notes area looks in a custom entity. There is no header area to create new notes.

missing-header-in-notes

Compare this with the contact entity.

notes-area-contact-entity

As you can see, there is a textarea to create new notes, which seems to be missing in the custom entity. I looked into this using DevTools, and it appears to be display issue, as the DOM elements for creating the notes are still there. I quickly wrote this bookmarklet to temporarily show the create new notes text area in the form.

Sourcecode

(function () {
	let contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		let activityWall = contentPanels[0].contentDocument.querySelector('#notesWall div.header');
		if(activityWall && activityWall.style.display === 'none'){
			activityWall.style.display = ''
		}
	} else {
		alert('Entity form not detected');
	}
})();

Bookmarklet

javascript:(function(){let contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){let activityWall=contentPanels[0].contentDocument.querySelector('#notesWall div.header');if(activityWall&&activityWall.style.display==='none'){activityWall.style.display=''}}else{alert('Entity form not detected');}})();void 0;

This is how the custom form will look, after you run the bookmarklet.

after-bookmarklet-header-restore-in-notes

I am not sure what sets the “display” property to none. When the DOM element is initially created, it is created without the “display” property set and then some event handler appears to be modifying this. I set a DOM breakpoint on “Attributes modifications”, but the breakpoint is not retained when I refresh the page, and hence I am unable to catch the “display: none” being set on “#notesWall div.header”. I spent some time to investigate this from the pretty-printed source, but could not figure it out.

Executing large FetchXML with WebAPI

You can easily execute fetchxml in WebAPI using the “fetchXml” query parameter. But this “GET” method won’t work, if the fetchxml is too big. In this case, you have to use the “POST” method to execute the fetchxml.

Sample Request Header:

Accept: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0
Content-Type: multipart/mixed;boundary=batch_contactfetch

Sample Request Body:

--batch_contactfetch
Content-Type: application/http
Content-Transfer-Encoding: binary

GET https://[CRM URL]/api/data/v8.2/contacts?fetchXml=<fetch count="10" ><entity name="contact" ><attribute name="fullname" /></entity></fetch> HTTP/1.1
Content-Type: application/json
OData-Version: 4.0
OData-MaxVersion: 4.0

--batch_contactfetch--

Sample Code:

var req = new XMLHttpRequest();
req.open("POST", Xrm.Page.context.getClientUrl() + "/api/data/v8.2/$batch", true);
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "multipart/mixed;boundary=batch_contactfetch");
req.onreadystatechange = function() {
    if (this.readyState === 4) {
        req.onreadystatechange = null;
        if (this.status === 200) {
            var response = JSON.parse(this.response.substring(this.response.indexOf('{'),this.response.lastIndexOf('}')+1));
			console.log(response.value);
        } else {
            Xrm.Utility.alertDialog(this.statusText);
        }
    }
};

var body = '--batch_contactfetch\n'
body += 'Content-Type: application/http\n'
body += 'Content-Transfer-Encoding: binary\n'
body += '\n'
body += 'GET ' + Xrm.Page.context.getClientUrl()+'/api/data/v8.2/contacts?fetchXml=<fetch count="10" ><entity name="contact" ><attribute name="fullname" /></entity></fetch> HTTP/1.1\n'
body += 'Content-Type: application/json\n'
body += 'OData-Version: 4.0\n'
body += 'OData-MaxVersion: 4.0\n'
body += '\n'
body += '--batch_contactfetch--'

req.send(body);

Request Screenshot

Fetch Post Request.png

Response Screenshot

fetch-post-response