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