Convert to Case: Displaying email contents

There are two ways to create case from an incoming email:

  1. Automatic record creation rules
  2. Convert to Case button in the email command bar

converttocase.png

Once a case is created, there may be so many other activities associated with that case and the email that was used to create the case might be all the way down in the activity feed. For quick reference you might just want to see the email content right on the case itself.

If you try to copy the email’s description field that stores the email content into a multiline text field on the case, you’ll face issues because of rich text emails. Most email sent these days are HTML emails, and Dynamics CRM doesn’t have a rich text field yet. So when you copy a rich text content into a field you will get everything i.e. content+markup. For example below is a simple email sent from outlook.

outlookemail

This is what will be copied to the text field on the case, when you copy the email entity’s description field. Now I will show you an approach to do this using a HTML webresource and Javascript. Below is the code for the HTML webresource that is embedded into the Case form.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Email Body</title>
</head>
<body>
	<h3>Case Source Email</h3>
	<div id="description"></div>
</body>
<script>
	var RYR = window.RYR || {};
	var Xrm = parent.Xrm;
	
	RYR.retrieveEntityWebApi = function(entityName,recordId,additionalCriteria) {
		var headers = new Headers({
		"Accept": "application/json",
		"Content-Type": "application/json; charset=utf-8",
		"OData-MaxVersion": "4.0",
		"OData-Version": "4.0"
		});
			
		fetch("/api/data/v8.0/"+entityName+"?$select=activityid,description&$filter=_regardingobjectid_value eq "+recordId, 
		{   method: 'GET',
			headers: headers,
			credentials: 'include' //without this header, the request will fail
		})
		.then(function(response){
			return response.json();
		})
		.then(function(c){
			if(c.value && c.value.length > 0) {
				document.getElementById('description').innerHTML = c.value[0].description;
				Xrm.Page.ui.tabs.get('general').sections.get('emailsection').setVisible(true);
			}
		})
		.catch(function(err) {
			console.log(err);
		});
	};
	
	RYR.retrieveEntityOData = function(entityName,recordId,additionalCriteria,callback) {
		var odataUrl = "/XRMServices/2011/OrganizationData.svc/"+entityName+"?$select=ActivityId,Description&$filter=RegardingObjectId/Id eq (guid'"+recordId+"')";
		if(additionalCriteria) {
			odataUrl += additionalCriteria;
		}				
		var retrieveReq = new XMLHttpRequest();
		retrieveReq.open("GET", odataUrl, false);
		retrieveReq.setRequestHeader("Accept", "application/json");
		retrieveReq.setRequestHeader("Content-Type", "application/json; charset=utf-8");
		retrieveReq.onreadystatechange = function () {
			if(callback) {
				callback(this);
			}
		};
		retrieveReq.send();
	};	
	
	var processEmailResults = function (response) {
			if (response.readyState == 4) {
				var emails = JSON.parse(response.responseText).d.results;
				if(emails.length > 0) {
					document.getElementById('description').innerHTML = emails[0].Description;
					Xrm.Page.ui.tabs.get('general').sections.get('emailsection').setVisible(true);
				}
			}
	};
	
	var retrieveEmailBody = function() {
		var recordId = Xrm.Page.data.entity.getId().substr(1,36);//Get rid of '{' and '}'
		if(!window.fetch ||
			!Xrm.Page.context.getVersion ||
			Xrm.Page.context.getVersion().split('.')[0] !== '8') {
			RYR.retrieveEntityOData('EmailSet',recordId,'&$top=1&$orderby=CreatedOn',processEmailResults);
		}
		else {
			RYR.retrieveEntityWebApi('emails',recordId,'&$top=1&$orderby=CreatedOn');
		}
	};
	
	if(Xrm.Page.ui.getFormType() === 2) retrieveEmailBody();
	
	window.RYR = RYR;
</script>	
</html>

This is how it looks on the Case form

Case

As I was just experimenting with various features, I have tried two approaches: standard XHR with the old OData endpoint and using fetch API with the Web API endpoint in Dynamics CRM 2016. I believe this is a better approach going forward. You can just include fetch polyfill and start using fetch today.

I initially didn’t want to try a code based approach. I created a Quick View Form on the email entity, stored the initial email record as a lookup field in the case and then use the Quick View Form to display the email on the case. But it didn’t display the email content.

I suspect this is because of the  warning “The email below might contain script or content that is potentially harmful and has been blocked.” displayed on the email. My theory is that once the email is tagged as unsafe by CRM, you cannot use Quick View Form to display its contents.

emailwarning

References:

  1. https://developer.mozilla.org/en/docs/Web/API/Fetch_API
  2. https://davidwalsh.name/fetch
  3. MSDN – Web API EntityType Reference
  4. MSDN – Retrieve an entity using Web API
  5. MSDN – Query data using Web API
Advertisements

ExchangeOnline and CRM Server Side Sync

There are two ways to quickly get access to a new Dynamics CRM Trial:

  1. https://www.microsoft.com/en-au/dynamics/crm-free-trial-overview.aspx
  2. https://www.microsoftdynamicsdemos.com/

If you create a new trial using the first link, you just get access to Dynamics CRM. But if you are looking to do some customer demos, it is better to create a trial using the 2nd link as it comes with SharePoint online and Exchange Online.

These are the demos I generally create:

DemoBuilder

Eventhough the screenshot still says CRM2015 Update 1, both these demos create a CRM2016 organisation. I am not sure about other regions, but this is the case with crm6 (Australia).

One of the issues I faced in when using orgs created using DemoBuilder is “The mailbox location could not be determined while sending/receiving the email message” error. I faced this error while configuring the CRM to ExchangeOnline server side sync. This issue is not consistently reproducable i.e. it occured in one org, but not in the other

Ideally you expect this to work without any additional configuration, as all the orgs built using DemoBuilder should have everything setup properly. But if the server side sync between CRM and ExchangeOnline is not working in your org due to this error, you can try the steps below as per KB2972143 (The article is for CRM2013 but the same resolution is applicable for CRMOnline running 8.0.0).

  1. Open the Microsoft Exchange Online Email Server Profile record from Settings -> Email Configuration -> Email Server Profiles
  2. Get the Godmode Bookmarklet and run it on the Microsoft Exchange Online Email Server Profile record
  3. Enter https://outlook.office365.com/EWS/Exchange.asmx for both Incoming Server Location and Outgoing Server Location
  4. Set Auto Discover Server Location to No
  5. Save the record

After following these steps hit the “Test and Enable Mailbox” button and the server side sync should start working.

Using window.postMessage to interact with IFrame in CRM Form

This is similar to my earlier post (https://dreamingincrm.com/2015/10/27/using-sessionstorage-to-interact-with-iframe-in-crm-forms/) that discussed using sessionStorage events to interact with IFrame and the IFrame to interact with the ClientApiWrapper. ClientApiWrapper is the IFrame that hosts the form scripts. Here is the code I used.

IFrame

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Form IFrame</title>
	<script>
	window.addEventListener('message', function(e) {
		if(e.origin != parent.Xrm.Page.context.getClientUrl()) return;
		document.getElementById('messageFromParent').textContent = 'Parent says "' + e.data+'"';	 
	});
		
	document.addEventListener('DOMContentLoaded' , function() {
		document.getElementById("buttons").addEventListener("click", function(e){
			var url = parent.Xrm.Page.context.getClientUrl();
			var fieldName = document.getElementById("crmFieldName").value;
			switch(e.target.id){
				case "enableField":
					parent.postMessage(['setFieldEditability', [fieldName,false]],url);
					break;
				case "disableField":
					parent.postMessage(['setFieldEditability', [fieldName,true]],url);
					break;
				case "hideField":
					parent.postMessage(['setFieldVisibility', [fieldName,false]],url);
					break;
				case "showField":
					parent.postMessage(['setFieldVisibility', [fieldName,true]],url);
					break;
			}
		});
	});
	</script>
</head>
<body>
	<p id="messageFromParent"></p><br><br>
	<input type="text" id="crmFieldName"><br><br>
	<div id="buttons">
	<button id="enableField">Enable Field</button>
	<button id="disableField">Disable Field</button>
	<button id="hideField">Hide Field</button>
	<button id="showField">Show Field</button>
	</div>	
</body>
</html>

Form Script

(function(){
var CVN = window.CVN || {};
var childChannel;
parent.addEventListener('message', function(e) {
if(e.origin != Xrm.Page.context.getClientUrl()) return;
childChannel = e.source;
CVN[e.data[0]].apply(null,e.data[1]);
}, false);

CVN.onSave = function(){
};

CVN.setFieldEditability = function(fieldName, isDisabled){
Xrm.Page.getControl(fieldName).setDisabled(isDisabled);
if(childChannel) childChannel.postMessage(fieldName+' has been '+(isDisabled ? 'disabled' : 'enabled'),Xrm.Page.context.getClientUrl());
};

CVN.setFieldVisibility = function(fieldName, isVisible){
Xrm.Page.getControl(fieldName).setVisible(isVisible)
if(childChannel) childChannel.postMessage(fieldName+' has been '+(isVisible ? 'hidden' : 'unhidden'),Xrm.Page.context.getClientUrl());
};

CVN.onLoad = function(){
};

window.CVN = CVN;
})();

Screenshot

Form

The eventhandler for message has to be attached to the parent for messages originating from both the form script (ClientApiWrapper) and the HTML webresource embedded into the form.

I envision the use of this technique primarily for form script -> html webresource communication, and common functions that are called by both form script and html webresource.

Reference: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

Gotcha: Plugin Running Async

Generally when you want run a set of tasks that are not realtime, you would do this via Workflow. On the contrary when you want something to happen straightaway, you would do this using a Plugin. Before CRM2015, there was only one type of workflow. Starting with CRM2015 a workflow can be realtime or async. A plugin also can be run asynchronously or synchronously. To summarise quickly:

  1. A realtime workflow is similar to a plugin
  2. An asynchronous plugin is similar to a workflow

Take this particular scenario: You have a plugin that runs synchronously on the Update of a certain entity. Later down the track, you decide you want this plugin to run async, as the set of tasks performed by the plugin doesn’t really have to be realtime and you want to improve the performance of the core operation.

In this scenario, you have to be mindful of one particular plugin behaviour: transaction. When an exception is thrown inside a plugin, the core operation won’t succeed as the exception in the plugin will cause the transaction to rollback. When you change the Execution Mode to “Asynchronous”, you will still see that the plugin has failed in System Jobs area, but the transaction won’t be rolled back.

For eg. take this simple code:

using Microsoft.Xrm.Sdk;
using System;

namespace CrmExperimentPlugin
{
    public class CreateContactPlugin : IPlugin
    {
        #region Secure/Unsecure Configuration Setup
        private string _secureConfig = null;
        private string _unsecureConfig = null;

        public CreateContactPlugin(string unsecureConfig, string secureConfig)
        {
            _secureConfig = secureConfig;
            _unsecureConfig = unsecureConfig;
        }
        #endregion
        public void Execute(IServiceProvider serviceProvider)
        {
            ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = factory.CreateOrganizationService(context.UserId);
            Entity entity = (Entity)context.InputParameters["Target"];

            Entity contact = new Entity("contact");
            contact["firstname"] = "Max";
            contact["lastname"] = "Power";
            service.Create(contact);

            throw new InvalidPluginExecutionException("Exception raised after contact create");
        }
    }
}

Assuming that the plugin is registered post-create on a particular entity and Execution Mode is “Synchronous”, there will be no contact called “Max Power” at the end of the plugin execution. But if the same plugin is registered as post-create and asynchronous, there will be a contact called “Max Power” as the exception doesn’t rollback the transaction.

If you reference Event Execution Pipeline in msdn, it says this about the plugin transaction:

Stages 20 and 40 are guaranteed to be part of the database transaction while stage 10 may be part of the transaction.

But this is not entirely true, as it also depends on the Execution Mode.

tl;dr; Register plugins to run sychronously, if you need the transaction to rollback on plugin exception and need to validate certain criteria for the core operation to succeed. If you don’t need these features, use a workflow.

Using sessionStorage to interact with IFrame in CRM Forms

CRM2015 Update 1 loads form scripts in seperate IFrame called ClientApiWrapper. I have seen a couple of posts in CRM forums asking how to interact with the IFrame from the form scripts and vice versa. The usual route is “window.parent”, but I tried to do this using sessionStorage and it is doing what I want.

Form Script

(function(){
	var RYR = window.RYR || {};
	
	window.addEventListener('storage', function(e) {
		if(e.key.split(':')[0] === 'child') {
			var functionToCall = e.key.split(':')[1];
			var sessionStoredValue = JSON.parse(e.newValue);
			RYR[functionToCall]
			.apply(null,
			Object.keys(sessionStoredValue)
			.map(function(d){ 
				return sessionStoredValue[d];
			}));
			sessionStorage.removeItem(e.key);			 
		}
	});
	
	RYR.onSave = function(){
	};
	
	RYR.setFieldEditability = function(fieldName, isDisabled){
		Xrm.Page.getControl(fieldName).setDisabled(isDisabled);
	};
	
	RYR.setFieldVisibility = function(fieldName, isVisible){
		Xrm.Page.getControl(fieldName).setVisible(isVisible)
	};	
	
	RYR.onLoad = function(){
		//temp workaround: iframe storage eventhandler takes some time to attach
		setTimeout(function(){ sessionStorage.setItem('parent', 'Form Load Event..'); }, 2000);
		Xrm.Page.getAttribute('ryr_name').addOnChange(function(){ 
			sessionStorage.setItem('parent', 'Name field changed..'); 
		});
	};
	
	window.RYR = RYR;
})();

HTML Webresource embedded as form IFrame

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Form IFrame</title>
	<script>
	window.addEventListener('storage', function(e) {
		if(e.key === 'parent') {
			document.getElementById('messageFromParent').textContent = 'Parent says "' + e.newValue+'"';
			sessionStorage.removeItem(e.key);			 
		}
	});
		
	document.addEventListener('DOMContentLoaded' , function() {
		document.getElementById("buttons").addEventListener("click", function(e){
			switch(e.target.id){
				case "enableField":
					sessionStorage.setItem('child:setFieldEditability', JSON.stringify({fieldName: document.getElementById("crmFieldName").value, disabled: false}));
					break;
				case "disableField":
					sessionStorage.setItem('child:setFieldEditability', JSON.stringify({fieldName: document.getElementById("crmFieldName").value, disabled: true}));
					break;
				case "hideField":
					sessionStorage.setItem('child:setFieldVisibility', JSON.stringify({fieldName: document.getElementById("crmFieldName").value, isVisible: false}));
					break;
				case "showField":
					sessionStorage.setItem('child:setFieldVisibility', JSON.stringify({fieldName: document.getElementById("crmFieldName").value, isVisible: true}));
					break;
			}
		});
	});		
	</script>
</head>
<body>
<input type="text" id="crmFieldName">




<div id="buttons">
<button id="enableField">Enable Field</button>
<button id="disableField">Disable Field</button>
<button id="hideField">Hide Field</button>
<button id="showField">Show Field</button>
</div>




</body>
</html>

There are event listeners on both the form script as well as the IFrame. The IFrame puts values in keys in this form: “child:{function name on form script}”. The form script uses just one key: parent.

Screenshots

FormLoad

NameChanged

With this technique, the form script doesn’t have to directly manipulate anything on the embedded IFrame or vice versa. In my example, I have the IFrame storing JSON to the sessionStore. This is retrieved by the event handler to call the correct function in the form script. The parent form script simply stores a string to the session store and this is displayed in the IFrame. This has to be changed if the requirements are different.

Reference: https://developer.mozilla.org/en/docs/Web/API/Window/sessionStorage

Using TypeScript and Gulp for CRM Client side scripting

Business Rules and real time workflows have reduced the necessity to write Javascript in Microsoft CRM for really simple use cases. Once the requirements are a bit more complex and client side scripting seems the right choice, you can try using TypeScript.

Pre-requisites

  1. Install Node
  2. Globally install gulp by typing “npm install gulp -g” into the command prompt
  3. Globally install TypeScript compiler by typing “npm install typescript -g” in command prompt.
  4. Install VSCode

package.json

{
  "devDependencies": {
    "babel-core": "^5.8.25",
    "del": "^2.0.2",
    "gulp": "^3.9.0",
    "gulp-babel": "^5.3.0",
    "gulp-load-plugins": "^1.0.0",
    "gulp-size": "^2.0.0",
    "gulp-sourcemaps": "^1.6.0",
    "gulp-tsc": "^1.1.1",
    "gulp-uglify": "^1.4.2"
  },
  "engines": {
    "node": ">=0.10.0"
  },
  "private": true
}

Once you download the repo from https://github.com/rajyraman/crm-client-scripts/tree/master/typescript run “node install” inside the typescript folder from command prompt. This will bring across the required node modules.

The below is the config file for gulp.

import gulp from 'gulp';
import del from 'del';
import gulpLoadPlugins from 'gulp-load-plugins';
import source from 'vinyl-source-stream';
import buffer from 'vinyl-buffer';
import glob from 'glob';
import es from 'event-stream';

const $ = gulpLoadPlugins();

gulp.task('build', ['clean'], ()=> {
    glob('src/**/**form.ts', function(err, files) {
        var tasks = files.map(function(entry) {
            let fileName = entry.substr(entry.lastIndexOf('/')+1).replace('.ts','.bundle.js');
            return gulp.src([entry])
                .pipe($.tsc({keepTree: false, out: fileName}))
                .pipe(buffer())
                .pipe($.sourcemaps.init({loadMaps: true}))
                .pipe($.uglify({preserveComments: 'some'}))
                .pipe($.sourcemaps.write())
                .pipe(gulp.dest('build'));
            });
        return es.merge.apply(null, tasks);
    });
    gulp.watch(['src/**/*.ts'], ['build']);
});

// Clean output directory
gulp.task('clean', cb => del(['.tmp', 'build/*', '!build/.git'], {dot: true}, cb));

gulp.task('default', ['build']);

In this build file, I make these assumptions:

  1. Scripts for each entity are stored in a separate folder
  2. The main entity script is suffixed with .form

Since the dependant files are always included via the triple slash reference comment on all the “.form.ts” files, TypeScript compiler will package up the output Javascript file with all the required dependencies.

There is also a gulp watch in the build task, so that any changes you make to the TypeScript files, triggers the build task and hence the compiled Javascript files will always be in sync with the TypeScript files. In order to start build, just type “gulp” in the command prompt from the folder containing the gulp.babel.js and the default task should take care of the rest.

TypeScript Definitions

Dave Berry has published the tsd files for CRM Client side development. There are so many good reasons to use TypeScript, and Intellisense is one of the top items in my list. There are community published tsds for other popular libraries and so you are not losing any thing by switching to TypeScript.

Head to the TypeScript playground to quickly familiarise yourself with the features and see how your TypeScript code is compiled into Javascript. Here is a quick comparison on couple of the common ones, in terms of CRM development with Dave Berry’s tsd files.

Javascript Typescript
Xrm.Page.getAttribute(‘[NAME]’).setValue(‘hello’) Xrm.Page.getAttribute(‘[NAME]’).setValue(‘hello’)
Xrm.Page.getControl(‘[NAME]’).setDisabled(false) Xrm.Page.getControl(‘[NAME]’).setDisabled(false)
Xrm.Page.getAttribute(‘[NAME]’).setValue([{id: ‘[ID]’, name: ‘[NAME]’, entityType: ‘[TYPE]’}]) Xrm.Page.getAttribute(‘[NAME]’).setValue([{id: ‘[ID]’, name: ‘[NAME]’, entityType: ‘[TYPE]’}])
Xrm.Page.getAttribute(‘[NAME]’).setValue(new Date()); Xrm.Page.getAttribute(‘[NAME]’).setValue(new Date());

These are the valid types for attributes types as per the tsd:

  1. NumberAttribute
  2. StringAttribute
  3. EnumAttribute (for Optionsets and bool)
  4. DateAttribute
  5. LookupAttribute

These are the valid control types as per the tsd:

  1. StandardControl
  2. GridControl
  3. FramedControl
  4. SilverlightControl

Why should you use TypeScript

Types is a polarizing topic. Some prefer dynamically typed language, while some prefer strongly typed. With ever increasing Javascript adoption, there is a growing opinion that types are a good thing. TypeScript still compiles into Javascript, but uses typing to improve developer productivity. It also adds some additional features that are not in Javascript. These are some of the advantages:

  1. Intellisense
  2. Compilation stage catches the obvious errors arising due to types
  3. Generics
  4. Modules (available in ES6 as well)
  5. Classes (available in ES6 as well)
  6. Union Types
  7. Microsoft product – great tooling support with VSCode and Visual Studio
  8. Open source

Screenshots

VSCode

output

Final notes

I have also added a sample project on how to do this with ES6, Babel and Browserify. You can refer https://github.com/rajyraman/crm-client-scripts/tree/master/es2015 for this, if you still want to use just Javascript.

EDIT (11/05/16): TypeScript has now published an official recipe for using Gulp, Browserify and Babel with TypeScript. Please refer http://www.typescriptlang.org/docs/handbook/gulp.html

DateTime AddMonths

I recently encountered a weird workflow behaviour that was caused due to my misunderstanding of how DateTime.AddMonths works. The issue happened with a recurring workflow. Since there is no recurring workflow functionality OOB in CRM there are couple of approaches to do this.

  1. Scheduling recurring Dynamics CRM workflows with FetchXML
  2. Asynchronous Batch Process Solution

I have used a slighly modified version of these approaches to do the recurring workflow functionality. There is only one instance of workflow running every day, and it basically uses fetchxml to grab records that meet a certain criteria and sends out notifications. One of my criteria is “start date 6 months before today” used for 6 month notification. But when you use DateTime.Now.AddMonths(-6) to populate the start date condition, I encountered this behavior.

Here is a quick summary of the issue

Current Date Code Expected Actual
28/08/2015 DateTime.Today.AddMonths(-6).ToString(“s”) 28/02/2015 28/02/2015
29/08/2015 DateTime.Today.AddMonths(-6).ToString(“s”) 01/03/2015 28/02/2015
30/08/2015 DateTime.Today.AddMonths(-6).ToString(“s”) 02/03/2015 28/02/2015
31/08/2015 DateTime.Today.AddMonths(-6).ToString(“s”) 03/03/2015 28/02/2015

So, when the workflow is running on these 4 days from 28/08/2015 to 31/08/2015 and calculating 6 months before the current date, it will produce the same date. As a result when the calculated date is used in a fetch condition as a filter, it will pick the same record(s) on these 4 dates.

The solution to fix your definition of month to 30 days, if that is acceptable. In my case it was OK, as all I did was send out notifications.

tldr; If the resulting day is not a valid day in the resulting month, the last valid day of the resulting month is used

Reference: https://msdn.microsoft.com/en-us/library/system.datetime.addmonths%28v=vs.110%29.aspx