One of the best features and biggest drawbacks to CRM 4.0's entity model is the ability to modify forms in CRM using Javascript. While this level of customization is something any sizable implementation will need, it's been reduced to pasting a bunch of Javascript code into a text box in CRM.
Not that I've got anything against pasting code in a textbox in CRM, but this isn't exactly an integrated development experience. In this post, I'll demonstrate how to have the onLoad() script in a CRM entity dynamically load one or more Javascript files (externally) and then run a load script once all the files have completely loaded.
First, let's talk about a little housekeeping. The image to your left (click to enlarge) represents the solution explorer view of a web project I've created that contains an ISV solution for CRM. There's some important things to note here.
First, I've got a folder called "Javascript". This folder houses my site-wide Javascript, and the basis for the OnLoad() handler that I'll bind to each entity. Let's take a look at some of the files in this folder.
| CrmFormHelper.js | Houses functions that help handle form events easily. After loaded by our dynamic scripting engine, the form helper is available to any form in CRM via the document.FormHelper object. |
| CrmFormIFrame.js | Houses functions that allow CRM to manipulate the IFRAME instance where a page appears |
| CrmWebService.js | Houses functions that easily allow CRM web services to be consumed through Javascript. |
| DiagnosticsWindow.js | When enabled, provides a trace window that shows events as they fire in CRM using the Javascript scaffolding in this article |
| OnLoad.js | Provides a stock OnLoad() implementation for your entities |
| OnSave.js | Provides a stock OnSave() implementation for your entities. |
For the purposes of this post, I'm going to focus on CrmFormHelper and OnLoad.js. What's in these fancy scripts, anyhow? Let's take a look:
CrmFormHelper.js 1: CrmFormHelper = function() {
2:
3: //Trace - Start
4: var TraceWindowHandle = null; var TraceLastTrace = new Date(); function TraceClear() { if (TraceWindowHandle == null) return; alert(TraceWindowHandle.document.body.innerHTML); TraceWindowHandle.document.body.innerHTML = '<body>'; TraceWindowHandle.document.writeln('<a href="javascript:opener.TraceClear();">Clear Window</a>'); } function Trace(cat, message) { if (typeof TraceWindowEnabled == 'undefined') return; message = message.split('<').join('<'); message = message.split('>').join('>'); if (TraceWindowHandle == null) { TraceWindowHandle = open('', 'TraceWindow', 'width=500,height=500,location=no,menubar=no,resizable=yes,left=200,top=20,directories=no,status=no,scrollbars=yes'); TraceWindowHandle.document.writeln('<a href="javascript:opener.TraceClear();">Clear Window</a>'); } try { date_now = new Date(); elapsedMS = date_now.getTime() - TraceLastTrace.getTime(); TraceLastTrace = date_now; TraceWindowHandle.document.writeln('<br>' + date_now + '(' + elapsedMS + ') ' + ':' + cat + ':' + message); TraceWindowHandle.window.scroll(0, TraceWindowHandle.document.body.scrollHeight + 10); } catch (err) { TraceWindowHandle = null; } } function TraceWriteRaw(markup) { if (typeof TraceWindowEnabled == 'undefined') return; if (TraceWindowHandle == null) { TraceWindowHandle = open('', 'TraceWindow', 'width=500,height=500,location=no,menubar=no,resizable=yes,left=200,top=20,directories=no,status=no,scrollbars=yes'); TraceWindowHandle.document.writeln('<a href="javascript:opener.TraceClear();">Clear Window</a>'); } try { TraceWindowHandle.document.writeln(markup); } catch (err) { TraceWindowHandle = null; } }
5: //Trace - End
6:
7: CrmFormHelper.prototype.IsEditForm = function() {
8: var CRM_Form_TYPE_EDIT = 2;
9:
10: if (crmForm.FormType == CRM_Form_TYPE_EDIT)
11: return true;
12: else
13: return false;
14: }
15:
16: CrmFormHelper.prototype.HandleFormOnLoad = function() {
17: Trace('HandleFormOnLoad', 'Starting');
18:
19: var CRM_Form_TYPE_CREATE = 1;
20: var CRM_Form_TYPE_EDIT = 2;
21:
22: switch (crmForm.FormType) {
23: case CRM_Form_TYPE_CREATE:
24: if (this.OnFormLoadCreate != null) {
25: Trace('HandleFormOnLoad', 'Calling OnFormLoadCreate');
26:
27: try {
28: this.OnFormLoadCreate();
29: }
30: catch (e) {
31: Trace("HandleFormOnLoad", "Error while calling OnFormLoadCreate: " + e.Description);
32:
33: debugger;
34: }
35:
36: Trace('HandleFormOnLoad', 'Done Calling OnFormLoadCreate');
37: }
38:
39: break;
40: case CRM_Form_TYPE_EDIT:
41: if (this.OnFormLoadEdit != null) {
42: Trace('HandleFormOnLoad', 'Calling OnFormLoadEdit');
43:
44: try {
45: this.OnFormLoadEdit();
46: }
47: catch (e) {
48: Trace("HandleFormOnLoad", "Error while calling OnFormLoadEdit: " + e.Description);
49:
50: debugger;
51: }
52:
53: Trace('HandleFormOnLoad', 'Done Calling OnFormLoadEdit');
54: }
55:
56: break;
57: }
58:
59: Trace('HandleFormOnLoad', 'Ending');
60: }
61:
62: CrmFormHelper.prototype.OnFormLoadCreate = function() {
63: Trace('OnFormLoadCreate', 'Default implementation called - no action taken');
64: }
65:
66: CrmFormHelper.prototype.OnFormLoadEdit = function() {
67: Trace('OnFormLoadEdit', 'Default implementation called - no action taken');
68: }
69:
70:
71: CrmFormHelper.prototype.HandleFormOnSave = function() {
72: Trace('HandleFormOnSave', 'Starting');
73: if (crmForm.IsDirty) {
74: Trace('HandleFormOnSave', 'Calling OnFormSaveDirty');
75: if (!this.OnFormSaveDirty()) {
76: Trace('HandleFormOnSave', 'OnFormSaveDirty setting return value to false, since onsavedirty returned false');
77: // Cancel the save operation.
78: event.returnValue = false;
79: }
80: Trace('HandleFormOnSave', 'Done Calling OnFormSaveDirty');
81: }
82: else {
83: Trace('HandleFormOnSave', 'Calling OnFormSaveClean');
84: this.OnFormSaveClean();
85: Trace('HandleFormOnSave', 'Done Calling OnFormSaveClean');
86: }
87: Trace('HandleFormOnSave', 'Ending');
88: }
89:
90: CrmFormHelper.prototype.OnFormSaveDirty = function() {
91: Trace('OnFormSaveDirty', 'Default implementation called - no action taken');
92: }
93:
94: CrmFormHelper.prototype.OnFormSaveClean = function() {
95: Trace('OnFormSaveClean', 'Default implementation called - no action taken');
96: }
97:
98: CrmFormHelper.prototype.GetDefaultLookupValue = function(guid, type, label) {
99: var lookupItem = new Array();
100: lookupItem[0] = new LookupControlItem(guid, type, label);
101: return lookupItem;
102: }
103: }
104:
105: document.FormHelper = new CrmFormHelper();
What does all this code do, anyhow? Well, basically, we're creating a Javascript class that can responsibly handle form events (load and save). This means that a different function can be called on Create or Edit mode for a form, and a different function can be called based on the form's state (clean or dirty) upon save. You'll also notice a bit of error handling code on each call, where we'll dump failures to the Trace window and launch the debugger. In our setup, all of the code above lives in an external Javascript file. This means, using Visual Studio 2008, you can set a breakpoint in any portion of this code and get full debugging support.
But how do I get this external code to load when my entity's form is displayed? Basically, you can copy and paste the following 40 lines of code in your entity's onLoad handler. Let's take a closer look.
1: function onLoad() {
2: window.DynamicScripts = new Array();
3: window.DynamicScriptsLoaded = 0;
4:
5: // *** BEGIN EDITS HERE *** Add any scripts you want to be loaded when the page begins, CrmFormHelper.js is required.
6: window.DynamicScripts[0] = "/ISV/Custom/Javascript/CrmFormHelper.js";
7: window.DynamicScripts[1] = "/ISV/Custom/Javascript/CrmWebService.js";
8: window.DynamicScripts[2] = "/ISV/Custom/Price List/Javascript/PriceListForm.js";
9: // *** END EDITS HERE ***
10:
11: //Trace - Start
12: var TraceWindowHandle = null; var TraceLastTrace = new Date(); function TraceClear() { if (TraceWindowHandle == null) return; alert(TraceWindowHandle.document.body.innerHTML); TraceWindowHandle.document.body.innerHTML = '<body>'; TraceWindowHandle.document.writeln('<a href="javascript:opener.TraceClear();">Clear Window</a>'); } function Trace(cat, message) { if (typeof TraceWindowEnabled == 'undefined') return; message = message.split('<').join('<'); message = message.split('>').join('>'); if (TraceWindowHandle == null) { TraceWindowHandle = open('', 'TraceWindow', 'width=500,height=500,location=no,menubar=no,resizable=yes,left=200,top=20,directories=no,status=no,scrollbars=yes'); TraceWindowHandle.document.writeln('<a href="javascript:opener.TraceClear();">Clear Window</a>'); } try { date_now = new Date(); elapsedMS = date_now.getTime() - TraceLastTrace.getTime(); TraceLastTrace = date_now; TraceWindowHandle.document.writeln('<br>' + date_now + '(' + elapsedMS + ') ' + ':' + cat + ':' + message); TraceWindowHandle.window.scroll(0, TraceWindowHandle.document.body.scrollHeight + 10); } catch (err) { TraceWindowHandle = null; } } function TraceWriteRaw(markup) { if (typeof TraceWindowEnabled == 'undefined') return; if (TraceWindowHandle == null) { TraceWindowHandle = open('', 'TraceWindow', 'width=500,height=500,location=no,menubar=no,resizable=yes,left=200,top=20,directories=no,status=no,scrollbars=yes'); TraceWindowHandle.document.writeln('<a href="javascript:opener.TraceClear();">Clear Window</a>'); } try { TraceWindowHandle.document.writeln(markup); } catch (err) { TraceWindowHandle = null; } }
13: //Trace - End
14:
15: TraceWindowEnabled = true;
16: Trace("Dynamic Scripts", "Script loading started");
17:
18: for (i = 0; i < window.DynamicScripts.length; i++) {
19: var script = document.createElement("SCRIPT");
20:
21: script.language = "javascript";
22: script.src = window.DynamicScripts[i] + "?nocache=" + Math.random();
23: script.onreadystatechange = function() {
24: if (this.readyState == "loaded" || this.readyState == "complete") {
25: window.DynamicScriptsLoaded += 1;
26:
27: Trace("Dynamic Scripts", this.src + " " + this.readyState);
28:
29: if (window.DynamicScriptsLoaded == window.DynamicScripts.length) {
30: Trace("Dynamic Scripts", "Script loading complete");
31:
32: if (document.FormHelper != null)
33: document.FormHelper.HandleFormOnLoad();
34: }
35: }
36: }
37:
38: document.getElementsByTagName("HEAD")[0].appendChild(script);
39: }
40: }
41:
Basically, lines 5-9 of the above snippet specify which external script files should be loaded before the page's CrmFormHelper's load methods are called. You can add any number of Javascript files here. Lines 18-36 dynamically build <SCRIPT> tags and add them to the header. At this point when all the scripts are loaded, then (and only then) is the FormLoad() fired (line 33). This guarantees that all of your script libraries are available to your OnLoad script.
So, what happens when the page loads? Looking at line 9 of the above code snippet, we see that one of the external javascript files is "/ISV/Custom/Price List/Javascript/PriceListForm.js". This external Javascript file is real small and houses 2 different functions that fire when my page loads (one for edit mode, one for creation).
1: /// <reference path="../../Javascript/CrmFormHelper.js" />
2: /// <reference path="../../Javascript/CrmWebService.js" />
3: /// <reference path="../../Javascript/Intellisense/new_custompricelist.js" />
4:
5: document.FormHelper.OnFormLoadCreate = function() {
6: alert("Form create was called");
7: }
8:
9: document.FormHelper.OnFormLoadEdit = function() {
10: alert("Form edit was called");
11: }
What's most important about this file is the first 3 lines. Those comments actually tell Visual Studio 2008 to load the specified Javascript files (which are loaded using our DynamicScript above) for interpretation in Intellisense. Yes, you got the right -- by referencing those external scripts in the first 3 lines of this file, you'll get full Intellisense support.
That concludes this post. In summary, using external Javascript files gets us a first class debugging experience in Visual Studio. Try it!
df34f5df-bb09-4f27-8cc9-19260a9a11a0|3|3.7
Development
dynamics crm, vsts, javascript