Cancelling save event based on the result of async operation

EDIT (06/02/2018):  Tanguy reported a scenario where the original code did not work when the user did a “saveandclose” instead of “save”. I have updated the code to handle this scenario. The uses the jQuery library on the parent frame to do the deep clone, but you could very well do the same using lodash clonedeep so that you don’t have to rely on CRM’s jQuery to do the job.

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;
	var uiClone = parent.jQuery.extend(true, {}, Xrm.Page.ui);
	var entityClone = parent.jQuery.extend(true, {}, Xrm.Page.data.entity);

	var closeHandler = ()=>{
		console.log('local. close blocked.');
	};

	var saveHandler = (ev)=>{
			console.log('local. 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.entity.save = entityClone.save;
					Xrm.Page.ui.close = uiClone.close;
					if((typeof ev === 'string' && ev === 'saveandclose') ||
						(ev.getEventArgs && ev.getEventArgs() && ev.getEventArgs().getSaveMode() === 2)){
						console.log('saveandclose');
						entityClone.save('saveandclose');
					}
					else{
						console.log('save');
						entityClone.save();
					}
				}
				else{
					console.log('User with homephone 12345 exists. Save blocked.');
				}
			});
	};

	return (e)=>{
		var eventArgs = e.getEventArgs();
		console.log(`DataXml OnSave: ${Xrm.Page.data.entity.getDataXml()}`);
		console.log(`Save Mode: ${eventArgs.getSaveMode()}`);
		if(isSave) {
			console.log('proceed to save');
			Xrm.Page.data.entity.save = entityClone.save;
			Xrm.Page.ui.close = uiClone.close;
			return;
		}
		else{
			Xrm.Page.data.entity.save = saveHandler;
			Xrm.Page.ui.close = closeHandler;
			if(eventArgs.getSaveMode() !== 2){
				eventArgs.preventDefault();
			}
			saveHandler(e);
		}
	}
})());

Result

Console Log Save Blocked

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

Advertisements

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.

Bug: Appointment organizer not being set

This bug was one the toughest for me to identify and implement a workaround as

  • It happens only one time in each session
  • Quick glance at the form during create does not provide any clues regarding the bug

The bug is this: When a new appointment is saved, the organizer field is not set even though it shows the field being populated during form load and save.

Here is how the appointment form looks when you create a new appointment:

Organizer

The organizer field appears to be populated, but it is not. This is the dataxml:

<appointment>
	<instancetypecode>0</instancetypecode>
	<prioritycode>1</prioritycode>
	<scheduledstart>2017-07-07T11:30:00</scheduledstart>
	<ownerid type="8" name="Natraj Yegnaraman">{40327227-A570-E611-80FA-005056A6A11C}</ownerid>
	<scheduleddurationminutes>30</scheduleddurationminutes>
	<isalldayevent>0</isalldayevent>
	<scheduledend>2017-07-07T12:00:00</scheduledend>
	<statuscode>5</statuscode>
	<transactioncurrencyid name="Australian Dollar" type="9105">{F0B901CE-7692-E211-82F1-0050569B4EC3}</transactioncurrencyid>
	<isbilled name="">false</isbilled>
	<ismapiprivate name="">false</ismapiprivate>
	<attachmenterrors name="">0</attachmenterrors>
	<isworkflowcreated name="">false</isworkflowcreated>
</appointment>

Compare this with the dataxml when the organizer field is really populated:

<appointment>
	<instancetypecode>0</instancetypecode>
	<organizer>
		<activityparty>
			<partyid type="8" name="Natraj Yegnaraman">{40327227-A570-E611-80FA-005056A6A11C}</partyid>
		</activityparty>
	</organizer>
	<prioritycode>1</prioritycode>
	<scheduledstart>2017-07-07T11:30:00</scheduledstart>
	<ownerid type="8" name="Natraj Yegnaraman">{40327227-A570-E611-80FA-005056A6A11C}</ownerid>
	<scheduleddurationminutes>30</scheduleddurationminutes>
	<isalldayevent>0</isalldayevent>
	<scheduledend>2017-07-07T12:00:00</scheduledend>
	<statuscode>5</statuscode>
	<transactioncurrencyid name="Australian Dollar" type="9105">{F0B901CE-7692-E211-82F1-0050569B4EC3}</transactioncurrencyid>
	<isbilled name="">false</isbilled>
	<ismapiprivate name="">false</ismapiprivate>
	<attachmenterrors name="">0</attachmenterrors>
	<isworkflowcreated name="">false</isworkflowcreated>
</appointment>

Also, notice that the Organizer field does not have a person icon next to the name, like the Owner field.

This is fix I implemented to workaround the issue:

	var organizer = Xrm.Page.getAttribute('organizer').getValue();
	if(Xrm.Page.ui.getFormType() === 1 && !organizer){
		Xrm.Page.getAttribute('organizer').setValue(Xrm.Page.getAttribute('ownerid').getValue());
	}

The script basically runs on form save provided it is a new record, and copies the Owner lookup value to Organizer lookup, if the Organizer lookup does not contain any value.

The reason the Organizer field is important is because, without this being set, that appointment won’t show up in the appointment creator’s Outlook  (even though they created it and they are also the owner).

This issue is happening in 8.1.0 and has been possibly fixed in 8.2.0.

Bug: Email replies with default signature

There appears to be a bug that affects this email replies when:

  • The email was created in CRM2015 and org was upgraded to CRM2016 OR the email was received from an external party AND
  • The user replies to the email from CRM2016/Dynamics 365

Email Signatures were added to CRM in 8.1. This feature enables an user to quickly setup their email signature(s). The user also has the capability to setup an email signature as default, so that it is automatically inserted into the email body. The bug is in the functionality.

When the user creates a new email, CRM always inserts div tag with id signature.

Email Signature New Email

Why does it insert this tag even though no signatures have been setup yet? It serves as a marker to know where to insert the signature in the reply: whether in the top or in the bottom.

When the user replies to an email which has a div#signature, then everything is fine. The signature is appended to the top of the email.

Email Signature div id signature exists

This is because when CRM sees that the user is trying to reply to an email which has a div#signature, it modifies the id to “oldsignature”, and appends a new div#signature to the DOM.

Email Signature oldsignature div.png

If the incoming email does not have div#signature in the DOM, CRM does not insert any div#signature to the reply at all. Even if you manually click, the “Insert Signature” button in the command bar, it inserts the signature to the bottom of the email which is not correct.

Email Signature in the bottom of reply.png

Resolution: Nothing at the moment, till MS fixes this issue.

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/

 

 

 

 

 

 

 

User not found error

Word of warning: This change is unsupported as it involves editing the CRM database directly. Use it at your own risk.

I encountered a weird error in CRM, that basically said that the user cannot be found. When I checked the event log, this appeared to be the underlying exception:

Exception type: CrmException
Exception message: No Microsoft Dynamics CRM user exists with the specified domain name and user ID
at Microsoft.Crm.Authentication.Claims.AuthenticationProvider.GetOrganizationId(ClaimsPrincipal principal)
at Microsoft.Crm.Authentication.Claims.AuthenticationProvider.Authenticate(HttpApplication application)
at Microsoft.Crm.Authentication.AuthenticationStep.Authenticate(HttpApplication application)
at Microsoft.Crm.Authentication.AuthenticationPipeline.Authenticate(HttpApplication application)
at Microsoft.Crm.Authentication.AuthenticationEngine.Execute(Object sender, EventArgs e)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

But, the user was indeed there from before and all I did was reactivate a long dormant user. My initial suspicion was that this had something to do with AD. I ran the following query to check the SID of the user against the MSCRM_CONFIG database:

select c.FriendlyName,b.FullName,d.AuthInfo,b.DomainName
from SystemUserOrganizations a
inner join [ORG_MSCRM].dbo.SystemUser b on a.CrmUserId=b.SystemUserId
inner join Organization c on c.Id=a.OrganizationId
inner join SystemUserAuthentication d on d.UserId=a.UserId
where b.DomainName='CONTOSO\powerm'

This gave me the SID of the user when the user was first created in CRM. I next ran this command in to get the current SID of the user:

wmic useraccount where name='powerm' get sid

The SID did not match to the one already in CRM, so I had to update it in the MSCRM_CONFIG database:

update d
set d.AuthInfo='W:[SID OUTPUT FROM THE DOS COMMAND PROMPT]'
from SystemUserOrganizations a
inner join [ORG_MSCRM].dbo.SystemUser b on a.CrmUserId=b.SystemUserId
inner join Organization c on c.Id=a.OrganizationId
inner join SystemUserAuthentication d on d.UserId=a.UserId
where b.DomainName='CONTOSO\powerm'

Once I updated this, the user was able to login with the same AD account.

Server Side Sync: View for Synced and Not-Synced Appointments

If you decide not to use the CRM App for Outlook, syncing emails, contacts and appointments using just Server Side Sync and Forward Mailbox can be really painful sometimes, especially when you are troubleshooting why something is not being synced. The “Server-Side Synchronization Monitoring” Dashboard provides some assistance to the Administrator, but when you want to dig into the details, I often end up using “FetchXML Builder” or “LinqPad” to query the TraceLog entity. Below is the fetchxml I use

<fetch top="50" >
  <entity name="tracelog" >
    <attribute name="tracestatus" />
    <attribute name="tracedetailxml" />
    <attribute name="tracelogid" />
    <attribute name="machinename" />
    <attribute name="tracecode" />
    <attribute name="traceactionxml" />
    <attribute name="traceparameterxml" />
    <attribute name="traceparameterhash" />
    <attribute name="errortypedisplay" />
    <attribute name="modifiedon" />
    <attribute name="text" />
    <attribute name="level" />
    <attribute name="collationlevel" />
    <filter>
      <condition attribute="tracestatus" operator="eq" value="0" />
    </filter>
    <order attribute="createdon" descending="true" />
  </entity>
</fetch>

Another common scenario that I troubleshoot is, why an appointment is not synced to Outlook after it was created in CRM. The first thing to check in this case is the user’s sync filter. I use the XrmToolBox tool “Sync Filter Manager” to check what the user’s sync filter for appointment. After confirming that the fetchxml picks up the appointment that has to be synced, we can now proceed to stage 2 of troubleshooting.

The field that is key in this scenario is called “GlobalObjectId“. This field in the appointment entity will be set, after the appointment is synced to the user’s Outlook. Unfortunately, this field is marked as not searcheable, which means it cannot be used in Advanced Find.

Global ObjectId.png

But, we can still create view that shows the synced appointments and appointments that have not been synced, using FetchXML Builder. Below are the steps

  1. Install FetchXML Builder from XrmToolBox store, if you don’t have it already
  2. Save an existing view into a new view on appointment entity. Don’t worry about the filters yet as we will update this using FetchXML BuilderSave a new view.png
  3. Next step is to open this view in FetchXML BuilderOpen View.png
  4. Update the fetchxml and save the view. Confirm the user’s sync filter for appointment using the Sync Filter Manager and make sure the new fetchxml criteria match with the user’s sync filter criteriaSave View.png

Synced Appointments – FetchXML

<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" >
  <entity name="appointment" >
    <attribute name="subject" />
    <attribute name="scheduledstart" />
    <attribute name="scheduledend" />
    <attribute name="regardingobjectid" />
    <attribute name="prioritycode" />
    <attribute name="activityid" />
    <attribute name="instancetypecode" />
    <attribute name="location" />
    <order attribute="createdon" descending="true" />
    <filter type="and" >
      <condition attribute="scheduledstart" operator="not-null" />
      <condition attribute="instancetypecode" operator="neq" value="2" />
      <condition attribute="scheduledend" operator="not-null" />
      <condition attribute="globalobjectid" operator="not-null" />
    </filter>
    	<link-entity name="activityparty" from="activityid" to="activityid" alias="ad" >
      <filter type="and" >
        <condition attribute="participationtypemask" operator="ne" value="9" />
      </filter>
    </link-entity>
  </entity>
</fetch>

Not Synced Appointments – FetchXML

<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" >
  <entity name="appointment" >
    <attribute name="subject" />
    <attribute name="scheduledstart" />
    <attribute name="scheduledend" />
    <attribute name="regardingobjectid" />
    <attribute name="prioritycode" />
    <attribute name="activityid" />
    <attribute name="instancetypecode" />
    <attribute name="location" />
    <order attribute="createdon" descending="true" />
    <filter type="and" >
      <condition attribute="scheduledstart" operator="not-null" />
      <condition attribute="instancetypecode" operator="neq" value="2" />
      <condition attribute="scheduledend" operator="not-null" />
      <condition attribute="globalobjectid" operator="null" />
    </filter>
    	<link-entity name="activityparty" from="activityid" to="activityid" alias="ac" >
      <filter type="and" >
        <condition attribute="participationtypemask" operator="ne" value="9" />
      </filter>
    </link-entity>
  </entity>
</fetch>

You should now be able to use these new view from the grid area.

Synced Appointments.png

If you try to use the view from Advanced Find, you will get this error.Advanced Find.png

This means you have to use FetchXML Builder to update you fetchxml, if any changes are required in the future. If you want to add more columns to the view, you’ll have to use “View Designer” to do that, as you would not be able to use Advanced Find to do this, because of this error.

I hope this post will help you troubleshoot future appointment sync issues.

Tools used:

  1. LinqPad
  2. Dynamics CRM LinqPad Driver
  3. XrmToolBox
  4. FetchXML Builder
  5. View Designer
  6. Sync Filter Manager