Sunday, March 8, 2009

Tips for Productive JavaScript Development in Dynamics CRM

I came to CRM from an application development background.  I’ve lived in Visual Studio.  I’m use to having IntelliSense, debugging, and I am use to getting instant results when testing my code.  When I first started doing JavaScript development in CRM I found myself thinking "There has to be a better way to do this!"  I shouldn’t have to publish a change, and make five clicks just to retest a scenario.  I also shouldn’t have to blindly code and hope my code is free of syntax errors (no intellisense).


I have found six tips especially helpful in JavaScript development.  Granted, some of these topics are not ground-breaking, but if you put them all together you will find yourself a much more productive JavaScript developer.


1)  Use an external JavaScript file while doing development.  It will save you a lot of time.  Regularly you make a code change and then you need to save and publish every time you want to retest.  Yes, the preview feature is helpful some of the time, but it is still very time consuming.   Using an external file will also allow you to leverage Visual Studio which offers IntelliSense and syntax checking.


Add this code to your onload.  It loads a JS file from within your ISV folder.   It uses a query string parameter to prevent caching and it waits until the dom is loaded before it executes the code.


var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type= 'text/javascript';
script.src= '/ISV/Entity/accounttesting.js?noCache=' +  Math.random();
script.onreadystatechange= function () {
if (this.readyState == 'complete' || this.readyState == 'loaded')
{
document.OnLoadCode()
}
}
head.appendChild(script);


Second, you can add calls in your OnSave and OnChange events to call code in the JavaScript file.  This will allow you to do all development in the one file.
OnSave:  document.OnSaveCode();
OnChange of account name:  document.OnChangeCode_AccountName();



Third, add a file name accounttesting.js to your /ISV/Entity folder under the website folder.  It will contain the following code.
document.OnLoadCode = function()
{
   alert("OnLoad code");
}


document.OnSaveCode = function()
{
   alert("OnSave code");
}



document.OnChangeCode_AccountName = function()
{
    alert("OnChange of Account Code");
}


// other functions

Last, start testing.  Any time you make a code change to the file you can refresh the account screen (F5) and the newest code will be executed.  Instant results!!!


Disclaimer:  You should only use this for development.  After you have tested your changes using this method you should copy the code back into the OnLoad and comment out the code that loads the JavaScript file.  Keeping the file in your OnLoad will save you time and headaches in deployment.  Plus, this is not supported.


2)  Use the debugger statement in your code if you want to walk through code.  You are able to leverage Visual Studio to analyze variable values as you execute your code.  Very useful!


First, within Internet Explorer, uncheck “Disable script debugging  (Internet Explorer)” and “Disable script debugger (Other)”



Next, add following code before any code you would like to debug.


debugger;



3) Use a helper library for calling the CRM web service through AJAX.    Calling the CRM web service requires you to work with the XML request using string concatenation which is very error-prone.  You can use my helper objects Click Here to help make life easier.  Ascentium has one as well Click Here.  Their library uses fetchxml while mine uses RetrieveMultiple.



4) Use Intellisense to help prevent mistakes as you code.  The book CRM as a Rapid Development Platform comes with a excellent Intellisense generator for Visual Studio.  http://www.thecrmbook.com/Video/Customization/EnableCRMJavascriptIntellisense/viewvideo.aspx.  It is well worth the 50 dollar investment.


You point the generator application against your instance of CRM and it generates JavaScript files for you to reference in your JS file.  Now you have Intellisense.  No more copy and pasting the field name from the forms window to make sure you don’t mis-spell it.  No more bugs because you spelled the word crmForm as crmform.




 


5)  Follow the DRY principle.  Don’t repeat yourself.  If you need to hide a field both onload and when a specific dropdown is selected, don’t copy and paste the code around.  Add functions to OnLoad and call the function wherever you need to.  Also, as you find useful code, such as hiding and showing fields, create functions for them and re-use them from project to project.


6)  Use try catch statements where possible.  Depending on your browser setings, it may be hard to determine what error is actually being thrown, especially in OnLoad.  Catching the error message and displaying an alert will help save time while troubleshooting errors.


try

   // Implementation here
}
catch (er)
{
        alert(er.message);
}


 -Andrew


This code is provided "AS IS" and contains no warranty and confers no rights.

Account Hierarchies in CRM 4.0

I recently collaborated with Ryan McCormick and Yaniv Arditi on the Dynamics forums at https://community.dynamics.com. We created a hierarchical representation of accounts in CRM that can be added as either a tool bar button or as an IFrame within account. The page can also display an icon next to the account or it can just display the account name. The entries link to the actual account records.

Without Icons





With Icons




Query
When pulling hierarchy data, one concern is performance. Each level in the hierarchy would require a call to the CRM webservice. Instead we went with a RecursionCTE query that used the filtered views.

with RecursionCTE (parentaccountid, accountid, [name])
as
(
select R2.parentaccountid, R2.accountid, R2.name from filteredaccount as R2
where r2.accountid = @accountid
UNION ALL
select R1.parentaccountid, R1.accountid, R1.name from filteredaccount as R1
join RecursionCTE as R2 on R1.parentaccountid = R2.accountid
)
select R1.new_pictureurl, R1.accountid, R1.name, case when r1.accountid =@accountid then null else R1.parentaccountid end as parentaccountid
from filteredaccount as R1
JOIN RecursionCTE as R2
on R1.accountid = R2.accountid


Adding Nodes to Treeview
I used an ASP.Net TreeView to display the hierarchy. I loaded the data into a dataset using a relationship and iterated through the dataset, adding nodes. I also checked to see that only four levels of nodes are shown. The reset are collapsed so the screen doesn’t become too cluttered.

… …
dsResult.Tables[0].TableName = "Account";
DataRelation relation = new DataRelation("ParentChild",
dsResult.Tables["Account"].Columns["accountid"],
dsResult.Tables["Account"].Columns["parentaccountid"],
true);
relation.Nested = true;
dsResult.Relations.Add(relation);

DataTable dt = dsResult.Tables[0];
DataRow[] rows = dt.Select(string.Format("accountid = '{0}'", acctId));
if (rows != null && rows.Length > 0)
{
DataRow row = rows[0];
TreeNode node = GetNode(row, true);
TreeView1.Nodes.Add(node);
DataRow[] childRows = row.GetChildRows(relation);
if (childRows.Length > 0)
AddChildNodesRecursive(node, childRows, relation, 1);

}
}
}


private void AddChildNodesRecursive(TreeNode parentNode, DataRow[] dataRows, DataRelation relation, int depth)
{
foreach (DataRow row in dataRows)
{
TreeNode node = GetNode(row, false);
parentNode.ChildNodes.Add(node);

DataRow[] childRows = row.GetChildRows(relation);
if (childRows.Length > 0)
AddChildNodesRecursive(node, childRows, relation, depth + 1);

if(depth >= _maxTreeDepth - 1) // set to 4
node.Collapse();

}
}



Displaying Image and URL
I set the navigate URL to the URL of the account. There is also an option to set an image URL to display an icon for the account. If none exists then a default icon can be used.

private static TreeNode GetNode(DataRow row, bool isRoot)
{
TreeNode node = new TreeNode();
node.Text = " " + row["name"].ToString();
node.Target = "_new";
node.NavigateUrl = string.Format("/{0}/SFA/accts/edit.aspx?id=" + row["accountid"].ToString(), ORG); ;

if (row["inet_imageurl"] == DBNull.Value)
// Set to some default Image
node.ImageUrl = "/isv/photos/defaultimage.gif";
else
node.ImageUrl = row["inet_imageurl"].ToString();

return node;
}




Opening Window without Toolbar and Sized Correctly
I ran into a problem with the way the account window opens. The target is set to _new so it will open in a new window, however you don’t have any control over how the window is displayed (width, height, show toolbar, ect). I had to intercept the button click and use JavaScript to open the new window. Looking back, it would have been better to render the HTML myself rather than relying on the tree view control. However, it does the job.

I had to set the onclick of the TreeView to


function postBackByObject()
{
if(window.event.srcElement.href != null
&& window.event.srcElement.href != ""
&& window.event.srcElement.href.substr(0,4).toLocaleLowerCase() == "http"
&& window.event.srcElement.nameProp.substr(0,11).toLocaleLowerCase() != "webresource")
{
window.open(window.event.srcElement.href,'popup','toolbar=no,location=no,directories=no,status=yes,scrollbars=yes,menubar=no,resizable=yes,width=1000,height=560');
}

// clicks that are expanding the treeview come in with webresource - let them occur
if(window.event.srcElement.href != null
&& window.event.srcElement.href != ""
&& window.event.srcElement.href.substr(0,4).toLocaleLowerCase() == "http"
&& window.event.srcElement.nameProp.substr(0,11).toLocaleLowerCase() != "webresource")
{
event.returnValue = false;
return false;
}
}

Adding to IFRAME
To add the page as an IFrame add a tab to the account entity and add a section. Next, add an IFRAME. Set "Pass record object type-code and unique identifier as paramters" as true. Also allow cross side scripting.



Set the necessary number of rows. Allow scrolling as necessary.




The IFrame shows up in a new tab.




Adding to Toolbar
To add the item as a toolbar item within account, add the following segment to the ISVConfig and re-import it.




See the attached code for the full implementation. Special thanks to Ryan McCormick and Yaniv Arditi for their help.

-Andrew


This release is provided "AS IS" and contains no warranty and confers no rights.