Convert Personal View to System View

When you design a query from the “Advanced Find” window, you can save the query for future use. I call this “Personal View”, but the official name for this is “Saved View”. This view is only visible to the person who saved the view (unless it is shared/reassigned).

As more personal views get created, it becomes an issue during migration, as these are not transported in the solution xml. You can of course use Solution Extender to copy this across, but I would like to do this right from the Advanced Find window. Apart from data migration, another scenario where you might find the need to create a system view from a personal view, is when you are constantly sharing views to a large number of people of group. When you are doing this, it is good time to actually make this a system view.

With these scenarios in mind, I have developed a solution that simplifies the process of creating a system view from a personal view. After installing the managed solution, you will see a new button called “Promote to System View” in the advanced find, when you switch to the “Saved Views” tab.

screenshot

To create a system view from the personal view, simply choose the views that need to be converted, and click the “Promote to System View” button. A message will be displayed after the conversion is complete. Once you refresh the window, you should be able to see the newly created system view(s).

Please log any issues/feedback/feature request in the github repo -> https://github.com/rajyraman/Personal-View-to-System-View/issues

You can download the managed solution from https://github.com/rajyraman/Personal-View-to-System-View/releases

Restricting the customer lookup

EDIT (07/03/2018)Updated the post for v9. In v9, you can use the supported approach of setEntityTypes and getEntityTypes. Refer https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/clientapi/reference/controls/setentitytypes

EDIT (04/07/2017): After testing this in 8.2.1, I have simplified the code as getLookupTypes and setLookupTypes can be used straight from the lookup itself, instead of going through getLookupDataAttribute.

First off, I have tested this only in CRMOnline on 8.1.0 and 8.2.1 so this might not work for you if you are are in older version. You can now easily restrict the default entity of the lookup, if it links to multiple types. e.g. customer lookup. The script below is for attribute of type “customer”, but you can follow a similar approach for “partylist” as well.

These new functions are undocumented, so they are technically unsupported (only 8.x. 9.x is documented and supported). So, use this at your own risk. In the script below, I am restricting a field with schemaname “customerid” and of type “customer” to “contact” entity only.

JavaScript (9.x) – Supported

var lookup = Xrm.Page.getControl('customerid');

//check if multiple type dropdowns enabled for this lookup
if (lookup.getEntityTypes().length > 1) {
    lookup.setEntityTypes(['contact']);
}

JavaScript (8.x) – Unsupported

var lookup = Xrm.Page.getAttribute('customerid');

//check if multiple type dropdowns enabled for this lookup and it is not a partylist. For partylist we might want to select an account and a contact
if (lookup.getLookupTypes().length > 1
     && !lookup.getIsPartyList()) {
    lookup.setLookupTypes(['contact']);
}

Screenshots

LookupLookup Search Customer

You can use this script during form load and easily restrict the lookup type.

Bookmarklet: Display Optionset values

Unless you have generated strongly typed classes, looking up Optionset values for the selected value or any other value, is one of the most annoying things to do as a developer. You can use the bookmarklet below to display Optionset values, along with the Optionset text for all the Optionsets on a form. You can drag the minified source to your bookmark bar, for creating the bookmarklet.

This is how a sample contact form looks after executing the bookmarklet.

Altered Optionset

Unminified

(function () {
	var contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		var Xrm = contentPanels[0].contentWindow.Xrm;
		var frameDocument = contentPanels[0].contentWindow.document;
		Xrm.Page.ui.controls.forEach(function (c) {
			if (c.getControlType() !== 'optionset')
				return;
			var attribute = c.getAttribute();
			var selectedOptionValue = attribute.getValue();
			var options = attribute.getOptions();
			var isClearOptions = options.some(function (o) {
					return o.text.indexOf(' (') === -1;
				});
			if (isClearOptions) {
				c.clearOptions();
			}
			options.forEach(function (o) {
				if (o.text && o.text.indexOf(' (') === -1) {
					o.text = o.text + ' (' + o.value + ')';
				}
				c.addOption(o);
			});
			if (selectedOptionValue && isClearOptions) {
				attribute.setValue(selectedOptionValue);
			}
		});
	} else {
		alert('Entity form not detected');
	}
})();

Minified

javascript:(function(){var contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){var Xrm=contentPanels[0].contentWindow.Xrm;var frameDocument=contentPanels[0].contentWindow.document;Xrm.Page.ui.controls.forEach(function(c){if(c.getControlType()!=='optionset')
return;var attribute=c.getAttribute();var selectedOptionValue=attribute.getValue();var options=attribute.getOptions();var isClearOptions=options.some(function(o){return o.text.indexOf(' (')===-1;});if(isClearOptions){c.clearOptions();}
options.forEach(function(o){if(o.text&&o.text.indexOf(' (')===-1){o.text=o.text+' ('+o.value+')';}
c.addOption(o);});if(selectedOptionValue&&isClearOptions){attribute.setValue(selectedOptionValue);}});}else{alert('Entity form not detected');}})();void 0;

Bookmarklet: Customise Form & Refresh Subgrids

If you just want to quickly customise the form layout use this bookmarklet below. Don’t add any new fields from this window, as it will create the field with the Default Publisher’s prefix, which by default is “new_”.

Unminified

var entityName = prompt("Entity?", "");
var url = Xrm.Page.context.getClientUrl() + '/main.aspx?etn=' + entityName + '&extraqs=formtype%3dmain&pagetype=formeditor';
window.open(url, '_blank');

Minified

javascript:var%20entityName=prompt("Entity?","");var%20url=Xrm.Page.context.getClientUrl()+'/main.aspx?etn='+entityName+'&extraqs=formtype%3dmain&pagetype=formeditor';window.open(url,'_blank');void%200;

If you just want to quickly refresh all the subgrids in the form, without refreshing the whole page, use this bookmarklet.

Unminified

(function () {
	var contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		var Xrm = contentPanels[0].contentWindow.Xrm;
		Xrm.Page.ui.controls.forEach(function (c) {
			if (c.getControlType() === 'subgrid') {
				c.refresh();
			}
		});
	} else {
		alert('Entity form not detected');
	}
})();

Minified

javascript:(function(){var%20contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return%20d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){var%20Xrm=contentPanels[0].contentWindow.Xrm;Xrm.Page.ui.controls.forEach(function(c){if(c.getControlType()==='subgrid'){c.refresh();}});}else{alert('Entity%20form%20not%20detected');}})();void%200;

Advanced Find and “In” condition

Advanced Find, in its current form, doesn’t have the capability to do a true “In” condition for text, datetime and numbers. For example this is how you would really do an “In” condition for a text field.

In condition

However the above condition would not return any results, as Advanced Find doesn’t allow separating valid values with a semi-colon. Query like the one above, has to be actually be done like the screenshot below.

In condition Actual

This is actually clunky and involves too many clicks. The query capability in Advanced Find also differs to what the workflow condition offers. For e.g. you can do a true “In” condition in workflow editor. The workflow condition builder uses a similar UI to Advanced Find, but it also displays a “In” condition for text attribute, which is not displayed in the Advanced Find.

Workflow In Condition

Workflow Step

Bookmarklet Solution

In order to do a “in” condition, where the target values are separated by semi-colons, you can use the bookmarklet below.

Unminified code

var contentPanel = $('#crmContentPanel > iframe');
if (contentPanel && contentPanel.length > 0) {
	var targetFrame = contentPanel[0].contentWindow;
	var isFetchModified = false;
	targetFrame.ExecuteQuery();
	var xml = targetFrame.$get("FetchXml").value;
	var xmlDoc = $.parseXML(xml),
	$xml = $(xmlDoc);
	$xml.find('condition[value]').each(function (i, d) {
		var inCondition = '';
		var multipleValues = $(d).attr('value');
		if (multipleValues.indexOf(';') === -1)
			return;
		multipleValues.split(';').forEach(function (c) {
			inCondition += ('<value>' + c + '</value>');
		});
		$(d).attr('operator', 'in');
		$(d).removeAttr('value');
		$(d).html(inCondition);
		isFetchModified = true;
	});
	if (isFetchModified) {
		targetFrame.$get("FetchXml", $get("resultRender")).value = '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true">' + xmlDoc.documentElement.innerHTML + '</fetch>';
	}
	targetFrame.changeArea(targetFrame.ResultsPage);
	targetFrame.$get('resultRender').submit();
} else {
	alert('Cannot locate Advanced Find Frame');
}
void 0;

Minified code for bookmark bar

javascript:var contentPanel=$('#crmContentPanel > iframe');if(contentPanel&&contentPanel.length>0){var targetFrame=contentPanel[0].contentWindow;var isFetchModified=false;targetFrame.ExecuteQuery();var xml=targetFrame.$get("FetchXml").value;var xmlDoc=$.parseXML(xml),$xml=$(xmlDoc);$xml.find('condition[value]').each(function(i,d){var inCondition='';var multipleValues=$(d).attr('value');if(multipleValues.indexOf(';')===-1)
return;multipleValues.split(';').forEach(function(c){inCondition+=('<value>'+c+'</value>');});$(d).attr('operator','in');$(d).removeAttr('value');$(d).html(inCondition);isFetchModified=true;});if(isFetchModified){targetFrame.$get("FetchXml",$get("resultRender")).value='<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true">'+xmlDoc.documentElement.innerHTML+'</fetch>';}
targetFrame.changeArea(targetFrame.ResultsPage);targetFrame.$get('resultRender').submit();}else{alert('Cannot locate Advanced Find Frame');}
void 0;

You basically continue to use the “equal” condition and then separate the valid values with semi-colon. Run the bookmarklet after that and the “equal” condition will be converted into a “in”. This currently works only for text fields. Datetime and number fields have additional format validations, hence it is not possible to use “;” to separate out the values.

I have tested this bookmarklet in CRM Online and CRM2015 with Firefox and Chrome.

Known Issues

A javascript error pops up before the results are displayed in Firefox. This can be ignored as the correct results are shown.

Bookmarklet: Turn off autosave & lookup on newwindow

The first bookmarklet turns off autosave for the current record, and refreshes the form without saving. Drag the below to your favorites bar.

javascript:(function(){var%20contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return%20d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){var%20Xrm=contentPanels[0].contentWindow.Xrm;Xrm.Page.data.refresh(false).then(function(){Xrm.Page.data.entity.addOnSave(function(econtext){var%20eventArgs=econtext.getEventArgs();if(eventArgs.getSaveMode()===70||eventArgs.getSaveMode()===2){eventArgs.preventDefault();}});alert('Form%20refreshed%20without%20save.%20Autosave%20turned%20off.');},function(errorCode,message){alert(message);});}else{alert('Entity%20form%20not%20detected');}})();void%200;

Below the unminified source.

(function () {
	var contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		var Xrm = contentPanels[0].contentWindow.Xrm;
		Xrm.Page.data.refresh(false).then(function () {
			Xrm.Page.data.entity.addOnSave(function (econtext) {
				var eventArgs = econtext.getEventArgs();
				if (eventArgs.getSaveMode() === 70 || eventArgs.getSaveMode() === 2) {
					eventArgs.preventDefault();
				}
			});
			alert('Form refreshed without save. Autosave turned off.');
		}, function (errorCode, message) {
			alert(message);
		});
	} else {
		alert('Entity form not detected');
	}
})();

The second bookmarklet open the selected lookup in a new window. Starting from CRM2015 every link you click inside a record form, opens in the same window. This can be annoying sometimes. To use this bookmarklet, you’ll just have to select the lookup on the record and execute the bookmarklet. This will open the lookup record in a new window. Below is the bookmarklet.

javascript:(function(){var%20contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return%20d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){var%20Xrm=contentPanels[0].contentWindow.Xrm;var%20currentControl=Xrm.Page.ui.getCurrentControl();if(currentControl.getControlType()==='lookup'){var%20currentLookup=currentControl.getAttribute().getValue();if(currentLookup){var%20entityName=currentLookup[0].type,entityId=currentLookup[0].id;var%20url=Xrm.Page.context.getClientUrl()+'/main.aspx?etc='+entityName+'&id='+entityId+'&newWindow=true&pagetype=entityrecord';window.open(url,'_blank');}}else{alert('The%20currently%20selected%20control%20is%20not%20a%20lookup');}}else{alert('Entity%20form%20not%20detected');}})();void%200;

This is the unminified source.

(function () {
	var contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		var Xrm = contentPanels[0].contentWindow.Xrm;
		var currentControl = Xrm.Page.ui.getCurrentControl();
		if (currentControl.getControlType() === 'lookup') {
			var currentLookup = currentControl.getAttribute().getValue();
			if (currentLookup) {
				var entityName = currentLookup[0].type,
				entityId = currentLookup[0].id;
				var url = Xrm.Page.context.getClientUrl() + '/main.aspx?etc=' + entityName + '&id=' + entityId + '&newWindow=true&pagetype=entityrecord';
				window.open(url, '_blank');
			}
		} else {
			alert('The currently selected control is not a lookup');
		}
	} else {
		alert('Entity form not detected');
	}
})();

References:

https://msdn.microsoft.com/en-us/library/gg509060.aspx

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

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

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

CRM UI Testing using Dalek

When it comes to UI testing there are plenty of options available. Some of the popular ones that I have encountered are

  • PhantomJS
  • CasperJS
  • Nightwatch
  • Selenium
  • Visual Studio Coded UI

Each of these frameworks have their own sets of benefits/drawbacks. I have been trying out DalekJS for the past few weeks, and I am really impressed by the ease of setup. For someone who is not really that much of a commandline/config json person, I really like not having to spend hours setting up config files and typing up commands in the shell.

Prerequisites

  1. Install node – Head to https://nodejs.org/ and install the correct edition of node for your machine
  2. Have a look at DalekJS Getting Started page at http://dalekjs.com/pages/getStarted.html. This basically involves installing the dalek CLI and dalek driver for Chrome browser. These are the commands you’ll have to type
    1. npm install dalek-cli -g
    2. npm init (This command will create the package.json. Straightforward questions to be answered in a wizard driven setup)
    3. npm install dalekjs –save-dev (This will update the package.json and add dalek as a dev dependency)
    4. npm install dalek-browser-chrome –save-dev (By default dalek uses PhantomJS which is a headless browser. If you want run your tests in Chrome, install this driver)

I ran some tests in PhantomJS and had issues, so I prefer to test this in Chrome.

Code
test.js

module.exports = (function () {
    var configJSON = require('./config');                                                                                                              
    try {
        return {
            'Login' : function (test) {
                test
                .open(configJSON.baseUrl)
                .type('#cred_userid_inputtext', configJSON.userName)
                .type('#cred_password_inputtext', configJSON.password + '\ue007\ue007')
                .wait(1000)
                .type('#cred_password_inputtext', '\ue007')
                .wait(5000)
                .toFrame('#InlineDialog_Iframe')
                .click('#butBegin')
                .done();
                test.screenshot('results/Login.png');
            },
            'Contacts' : function (test) {
                test.open(configJSON.baseUrl + '/main.aspx?etc=2&id='+configJSON.contactid+'&pagetype=entityrecord')
                .toFrame('iframe[title="Content Area"]')
                .execute(function () {
                        this.assert.ok(Xrm.Page.getAttribute('fullname').getValue() === 'Max Power', 'Name is Max Power');
                    })
                .done();
                test.screenshot('results/ContactName.png');
            }
        };
    }
    catch (err) {
        console.log(err);
    }
})();

config.js

{ "baseUrl": "http://abc123.crm6.dynamics.com",
"userName": "admin@abc123.onmicrosoft.com",
"password": "pass@word1",
"contactid": "{1005D93B-A22E-E511-80E2-C4346BC5B290}"
}

Here is how the project look in VS Code

Test code in VS Code

I am running the tests in a CRMOnline org, hence I have included Login as a test and the username and password is retrieved from the config.js file. This test is required, as the subsequent tests require the user be logged into CRMOnline. If you are running the OnPrem with integrated authentication, this test is not required.

When it comes to testing the form behaviour, you can approach this two ways: by looking the the DOM elements or using CRM Client API to confirm what should have happened, has happened. For eg. if a field is supposed to be hidden after a picklist option is changed to a certain value, you can either confirm this either by

  1. Accessing the css display property of that field elements in the DOM
  2. Using Xrm.Page.getControl(..).getVisible().

If you are heading the option 2 way, that means that you’ll have to execute a script against that page to validate the test. This is precisely what the execute method does. I like this approach, as I don’t have to fiddle with fieldname_c and fieldname_d divs, access innerText or anything related to the DOM. All the test results are captured as screenshots, so that we can visually confirm the results.

Running the Tests

Inorder to run the tests, you simply type dalek test.js -b chrome, where test.js is the file containing your tests. Executing this command will open the Chrome browser and start executing the tests. If you leave out “-b chrome” part the tests will be run in PhantomJS. I was not able to complete the tests using PhantomJS, as I had some issues with the toFrame part.

Here is how the test result screen looks like.

TestResults

DalekJS is well documented. You can head over to http://dalekjs.com/pages/documentation.html and have a look at the options available. This is one more tool you at your disposal to test your client side code for CRM.

Webdriver spec is under consideration by W3C. Once this is standardised and implemented by all major browsers, UI Testing should get lot easier. Below are the links you can look into, if you are interested in the WebDriver specs and the implementation status (Thank you @jacobrossi for tweeting this).

  1. http://www.w3.org/TR/2013/WD-webdriver-20130117/
  2. http://dev.modern.ie/platform/status/webdriver/details/
  3. https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver/status

EDIT (20/08/15): Updated script to switch to correct IFrame before using Xrm.Page.getAttribute, instead of using hacky frames[0] and frames[1]. I also added additional content about WebDriver spec.