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.

Saturday, February 21, 2009

CRM 4.0 Registration Problems with On Disk

I was working to install an workflow assembly. I was using the disk option, so I could debug the code. I build the code from the same server that I was installing from. The code that I was building was originally developed on another VPC. The VPC had a CRM installation the was upgraded from 3.0. This VPC was a fresh CRM 4.0 installation.

So, I navigated to the directory and grabbed it. I specified to go to disk and clicked "Register Selected Plugin."



The error returned is:
Unhandled Exception: System.Web.Services.Protocols.SoapException: Server was unable to process request.
Detail: 0x80044191
Assembly can not be loaded from C:\Program Files\Microsoft Dynamics CRM\server\bin\assembly\WorkflowMerge.dll.



When trying to do an install to database it went in fine. However, registering from disk wouldn't go. It ends up that the file needs to be stored in that exact directory or it returns the error. I didn't realize that it wasn't storing it in the right directory because of differences between environments.

Default location for storing assemblies assemblies on CRM 4.0 when upgraded from 3.0:
  • C:\Program Files\Microsoft Dynamics CRM Server\Server\bin\assembly\
Default location for storing assemblies assemblies on CRM 4.0 when a fresh CRM 4.0 installation:
  • C:\Program Files\Microsoft Dynamics CRM\Server\bin\assembly\.

Sunday, February 1, 2009

CRM 4.0 - JavaScript Web Service Helper Objects

When doing web service calls from JavaScript you are required to write code that concatenates XML strings and manually posts an HTTP request to CRM. The code looks something like this (courtesy of CRM 4.0 SDK).


Traditional Web Service Soap Request
var xml = "<?xml version='1.0' encoding='utf-8'?>"+
"<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'"+
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"+
" xmlns:xsd='http://www.w3.org/2001/XMLSchema'>"+
authenticationHeader+
"<soap:Body>"+
"<RetrieveMultiple xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"+
"<query xmlns:q1='http://schemas.microsoft.com/crm/2006/Query'"+
" xsi:type='q1:QueryExpression'>"+
"<q1:EntityName>contact</q1:EntityName>"+
"<q1:ColumnSet xsi:type='q1:ColumnSet'>"+
"<q1:Attributes>"+
"<q1:Attribute>fullname</q1:Attribute>"+
"<q1:Attribute>contactid</q1:Attribute>"+
"</q1:Attributes>"+
"</q1:ColumnSet>"+
"<q1:Distinct>false</q1:Distinct>"+
"<q1:Criteria>"+
"<q1:FilterOperator>And</q1:FilterOperator>"+
"<q1:Conditions>"+
"<q1:Condition>"+
"<q1:AttributeName>address1_city</q1:AttributeName>"+
"<q1:Operator>Like</q1:Operator>"+
"<q1:Values>"+
"<q1:Value xsi:type='xsd:string'>"+searchCity+"</q1:Value>"+
"</q1:Values>"+
"</q1:Condition>"+
"</q1:Conditions>"+
"</q1:Criteria>"+
"</query>"+
"</RetrieveMultiple>"+
"</soap:Body>"+
"</soap:Envelope>";


This code is only the SOAP body generation.  It doesn't even include the HTTP request or extracting the data.  You can imagine what a maintenance nightmare you have when your form requires multiple web service calls.  Wouldn't it be nice if the API was closer to the .NET SDK API?


Helper Objects
I have written a couple of helper classes to help make life easier. Here are two examples of what you can do with the helper classes. It doesn't currently support everything (grouping multiple levels of filter conditions), but it handles 90% of the cases you run into. As I add more functionality to the clases, I will repost them. 


Simple query on one entity
This query does a simple select of leads where the city is either Bloomington or Minneapolis.   Include this code as well as the helper objects from the bottom of the post.
var LOGICAL_OPERATOR_OR = "Or";
var CONDITION_OPERATOR_EQUAL = "Equal";

// Create object passing in the entity you are selecting from     
var crmService = new CrmService("lead", LOGICAL_OPERATOR_OR);
crmService.AddColumn("fullname");
crmService.AddColumn("leadid");


// Add filter conditions  (note:  the "OR" logical operator was specified in constructor)
crmService.AddFilterCondition("address1_city", "Bloomington", CONDITION_OPERATOR_EQUAL);
crmService.AddFilterCondition("address1_city", "Minneapolis", CONDITION_OPERATOR_EQUAL);


// Retrieve the result object
var result = crmService.RetrieveMultiple();


// Loop through rows and select values (they return strings)
for (rowsNumber in result.Rows) {
   var row = result.Rows[rowsNumber];
   // Get Column By Name
   alert(row.GetValue("fullname"));
   alert(row.GetValue("leadid"));
}


Query that Links in Multiple Tables
This selects accountnumber, accountid, and name from the account entity by specifying the ID of the contact.  Include this code as well as the helper objects from the bottom of the post.
var LOGICAL_OPERATOR_AND = "And";
var LOGICAL_OPERATOR_OR = "Or";
var CONDITION_OPERATOR_EQUAL = "Equal";
var JOINOPERATOR_INNER = "Inner";


// Create object passing in the entity you are selecting from      
var crmService = new CrmService("account", LOGICAL_OPERATOR_OR);


// Specify select columns
crmService.AddColumn("accountnumber");
crmService.AddColumn("accountid");
crmService.AddColumn("name");


// Define linked entity - similar to SDK overload
var entityLinked = crmService.AddLinkedEntityCondition("account", "contact", "accountid", "parentcustomerid", JOINOPERATOR_INNER)


// Set filter operator (AND, OR, Ect)
entityLinked.FilterOperator = LOGICAL_OPERATOR_AND;


// Add filter condition (can add as multiple)
entityLinked.AddFilterCondition("contactid", "{BB1F590A-37D0-DC11-AA32-0003FF33509E}", CONDITION_OPERATOR_EQUAL);


// Retrieve the result object
var result = crmService.RetrieveMultiple();


// Loop through rows and select values (they return strings)
for (rowsNumber in result.Rows) {
   var row = result.Rows[rowsNumber];
   // Get Column By Name
   alert(row.GetValue("accountnumber"));
   alert(row.GetValue("name"));
   alert(row.GetValue("accountid"));
}


Helper Objects - Simply copy into the top of your form load
var LOGICAL_OPERATOR_AND = "And";
var LOGICAL_OPERATOR_OR = "Or";
var CONDITION_OPERATOR_LIKE = "Like";
var CONDITION_OPERATOR_EQUAL = "Equal";
var CONDITION_OPERATORNOT_EQUAL = "NotEqual";
var JOINOPERATOR_INNER = "Inner";
var JOINOPERATOR_LEFTOUTER = "LeftOuter";
var JOINOPERATOR_NATURAL = "Natural";



function CrmService(entityName, logicalOperator) {
    // Double check in case you pass a variable that hasn't been set
    // This error is hard to track down
    if (logicalOperator == null)
        throw new Error("Must specify non-null value for logicalOperator");


    if (entityName == null)
        throw new Error("Must specify non-null value for entityName");
    this.entityName = entityName;
    this.ColumnSet = new Array();
    this.LogicalOperator = logicalOperator;
    this.Conditions = new Array();
    this.LinkedEntities = new Array();
}



CrmService.prototype.getEntityName = function() {
    return this.entityName;
}


function Condition(field, value, operator) {
    this.Field = field;
    this.Value = CrmEncodeDecode.CrmXmlEncode(value);
    // Double check in case you pass a variable that hasn't been set
    // This error is hear to track down
    if (operator == null)
        throw new Error("Must specify non-null value for operator");
    this.Operator = operator;
}


CrmService.prototype.setEntityName = function() {
    return this.entityName;
}


CrmService.prototype.AddColumn = function(columnName) {
    this.ColumnSet[this.ColumnSet.length] = columnName;
}


CrmService.prototype.AddFilterCondition = function(field, value, conditionOperator) {
    this.Conditions[this.Conditions.length] = new Condition(field, value, conditionOperator);
}



function LinkedEntity(linkFromEntityName, linkToEntityName, linkFromAttributeName, linkToAttributeName, joinOperator) {
    this.LinkFromEntityName = linkFromEntityName;
    this.LinkToEntityName = linkToEntityName;
    this.LinkFromAttributeName = linkFromAttributeName;
    this.LinkToAttributeName = linkToAttributeName;
    if (joinOperator == null)
        throw new Error("Must specify non-null value for operator");
    this.JoinOperator = joinOperator;
    this.Conditions = new Array();
    this.FilterOperator = LOGICAL_OPERATOR_AND;
}


LinkedEntity.prototype.AddFilterCondition = function(field, value, conditionOperator) {
    this.Conditions[this.Conditions.length] = new Condition(field, value, conditionOperator);
    return this.Conditions[this.Conditions.length - 1];
}


CrmService.prototype.AddLinkedEntityCondition = function(linkFromEntityName, linkToEntityName, linkFromAttributeName, linkToAttributeName, joinOperator) {
    this.LinkedEntities[this.LinkedEntities.length] = new LinkedEntity(linkFromEntityName, linkToEntityName, linkFromAttributeName, linkToAttributeName, joinOperator);
    return this.LinkedEntities[this.LinkedEntities.length - 1];
}


function RetrieveMultipleResult(crmService) {
    this.Rows = new Array();
    this.CrmService = crmService;
}



RetrieveMultipleResult.prototype.AddRow = function() {
    this.Rows[this.Rows.length] = new Row();
    return this.Rows[this.Rows.length - 1];
}


 


function Row() {
    this.Columns = new Array();
}


function Column(columnName, value, dataType) {
    this.ColumnName = columnName;
    this.Value = value;
    this.DataType = dataType;
}


Row.prototype.AddColumn = function(columnName, value) {
    this.Columns[this.Columns.length] = new Column(columnName, value);
}


Row.prototype.GetColumn = function(columnName) {
    for (columnNumber in this.Columns) {
        var column = this.Columns[columnNumber];
        if (columnName.toLowerCase() == column.ColumnName.toLowerCase())
            return column;
    }
    throw new Error("Column " + columnName + " does not exist");
}


Row.prototype.GetValue = function(columnName) {
    var column = this.GetColumn(columnName);
    return column.Value;
}



CrmService.prototype.RetrieveMultiple = function() {


    //create SOAP envelope
    var xmlSoapHeader = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">";


    var xmlAuthHeader = GenerateAuthenticationHeader();


    var xmlSoapBody = "<soap:Body>" +
 "      <RetrieveMultiple xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">  " +
 "<query xmlns:q1=\"http://schemas.microsoft.com/crm/2006/Query\" xsi:type=\"q1:QueryExpression\">  " +
 "  <q1:EntityName>" + this.getEntityName() + "</q1:EntityName>  " +
 "  <q1:ColumnSet xsi:type=\"q1:ColumnSet\">  " +
 "    <q1:Attributes>  ";


    for (columnNumber in this.ColumnSet) {
        var column = this.ColumnSet[columnNumber];
        xmlSoapBody = xmlSoapBody + "          <q1:Attribute>" + column + "</q1:Attribute>";
    }


    xmlSoapBody = xmlSoapBody + "        </q1:Attributes>" +
 "      </q1:ColumnSet>" +
 "          <q1:Distinct>false</q1:Distinct>  " +
 "          <q1:PageInfo>  " +
 "            <q1:PageNumber>0</q1:PageNumber>  " +
 "            <q1:Count>0</q1:Count>  " +
 "          </q1:PageInfo>  " +
 "         <q1:LinkEntities>";


    if (this.LinkedEntities.length > 0) {
        for (linkedEntityNumber in this.LinkedEntities) {
            var linkedEntity = this.LinkedEntities[linkedEntityNumber];
            xmlSoapBody += " <q1:LinkEntity> ";
            xmlSoapBody += "                 <q1:LinkFromAttributeName>" + linkedEntity.LinkFromAttributeName + "</q1:LinkFromAttributeName> ";
            xmlSoapBody += "                 <q1:LinkFromEntityName>" + linkedEntity.LinkFromEntityName + "</q1:LinkFromEntityName> ";
            xmlSoapBody += "                 <q1:LinkToEntityName>" + linkedEntity.LinkToEntityName + "</q1:LinkToEntityName> ";
            xmlSoapBody += "<q1:LinkToAttributeName>" + linkedEntity.LinkToAttributeName + "</q1:LinkToAttributeName> ";
            xmlSoapBody += "<q1:JoinOperator>" + linkedEntity.JoinOperator + "</q1:JoinOperator> ";
            xmlSoapBody += "<q1:LinkCriteria> ";


            if (linkedEntity.FilterOperator == null)
                throw new Error("Must specify non-null value for FilterOperator");


            xmlSoapBody += " <q1:FilterOperator>" + linkedEntity.FilterOperator + "</q1:FilterOperator> ";
            xmlSoapBody += " <q1:Conditions> ";


            for (conditionLinkedNumber in linkedEntity.Conditions) {
                var conditionLinked = linkedEntity.Conditions[conditionLinkedNumber];
                xmlSoapBody += "                             <q1:Condition> ";
                xmlSoapBody += "                                             <q1:AttributeName>" + conditionLinked.Field + "</q1:AttributeName> ";
                xmlSoapBody += "                                             <q1:Operator>" + conditionLinked.Operator + "</q1:Operator> ";
                xmlSoapBody += "                                             <q1:Values> ";
                xmlSoapBody += "                                                             <q1:Value xsi:type=\"xsd:string\">" + conditionLinked.Value + "</q1:Value> ";
                xmlSoapBody += "                                             </q1:Values> ";
                xmlSoapBody += "                             </q1:Condition> ";
            }
            xmlSoapBody += " </q1:Conditions> ";
            xmlSoapBody += " <q1:Filters /> ";
            xmlSoapBody += "</q1:LinkCriteria> ";
            xmlSoapBody += "<q1:LinkEntities />";
            xmlSoapBody += "</q1:LinkEntity>";
        }
    }


    if (this.LogicalOperator == null)
        throw new Error("Must specify non-null value for LogicalOperator");



    xmlSoapBody += "</q1:LinkEntities>" +
 "          <q1:Criteria>  " +
 "            <q1:FilterOperator>" + this.LogicalOperator + "</q1:FilterOperator>  " +
 "            <q1:Conditions>  ";


 


    for (conditionNumber in this.Conditions) {
        var condition = this.Conditions[conditionNumber];


        if (condition.Operator == null)
            throw new Error("Must specify non-null value for condition Operator");


        xmlSoapBody += "              <q1:Condition>  " +
                "                <q1:AttributeName>" + condition.Field + "</q1:AttributeName>  " +
                "                <q1:Operator>" + condition.Operator + "</q1:Operator>  " +
                "                <q1:Values>  " +
                "                  <q1:Value xsi:type=\"xsd:string\">" + condition.Value + "</q1:Value>  " +
                "                </q1:Values>  " +
                "              </q1:Condition>  ";


    }


 



    xmlSoapBody += "            </q1:Conditions>  " +
 "            <q1:Filters />  " +
 "          </q1:Criteria>  " +
 "          <q1:Orders />  " +
 "        </query>  " +
 "      </RetrieveMultiple>  " +
 "    </soap:Body> " +
 "   </soap:Envelope>";



    var xmlt = xmlSoapHeader + xmlAuthHeader + xmlSoapBody;
    var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");
    xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
    xmlHttpRequest.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/RetrieveMultiple");
    xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    xmlHttpRequest.setRequestHeader("Content-Length", xmlt.length);
    xmlHttpRequest.send(xmlt);


    if (xmlHttpRequest.responseXML == null || xmlHttpRequest.responseXML.xml == null || xmlHttpRequest.responseXML.xml == "") {
        if (xmlHttpRequest.responseText != null && xmlHttpRequest.responseText != "")
            throw new Error(xmlHttpRequest.responseText);
        else
            throw new Error("Error returning response");
    }


    var xmlResponse = xmlHttpRequest.responseXML.xml;
    if (xmlHttpRequest.responseXML.documentElement.selectNodes("//error/description").length > 0) {
        throw new Error(xmlResponse);
    }


    var objNodeList = xmlHttpRequest.responseXML.documentElement.selectNodes("//BusinessEntity");



    var totalNodesCount = objNodeList.length;


    var result = new RetrieveMultipleResult(this);


    var nodeIndex = 0;
    var fieldTextTemp = "";
    var fieldText = "";
    if (totalNodesCount > 0) {
        do {


            var row = result.AddRow();
            for (columnNumber in this.ColumnSet) {
                var columnName = this.ColumnSet[columnNumber];
                fieldText = "";
                var valueNode = objNodeList[nodeIndex].getElementsByTagName("q1:" + columnName)[0];
                if (valueNode != null) {
                    fieldTextTemp = valueNode.childNodes[0].nodeValue;
                    if (fieldTextTemp != null && fieldTextTemp != "") {
                        fieldText = fieldText + fieldTextTemp;
                    }
                }
                row.AddColumn(columnName, fieldText);
            }
            nodeIndex = nodeIndex + 1;
        }
        while (totalNodesCount > nodeIndex)
    }
    return result;
}

Sunday, January 18, 2009

CRM 4.0 - OnChange Firing after OnSave

While writing JavaScript customizations in CRM 4.0, I always assumed that onSave is the last event to occur before data saves to the database. However, I found a special cases where onChange gets fired after onSave. The scenario is when you are updating an entity such as account and the primary contact lookup field has a value. You blank out the lookup field and click save. The onChange event of primary contact fires after onSave. This was especially problematic if you have code in the onChange that blanks out fields on the form. The fields will save to the database blanked-out.

To get around this, I recommend adding code to any onChange event to prevent it from running after onSave.

OnLoad - Declare a public variable
document.IsSaving= false;

OnSave - Update the public variable
document.IsSaving= true;

OnChange - Exit out if the OnSave has Already Ran
if(document.IsSaving)
return;

// Some other logic

Friday, November 7, 2008

System.Data.SqlClient.SqlException: The query processor ran out of stack space during query optimization. Please simplify the query.

Every now and then we run into an error that comes out of the blue. The SQL update statement runs fine for years and one day they start throwing errors.

I had just such an issue this past week. Here is the error I received:

System.Data.SqlClient.SqlException: The query processor ran out of stack space during query optimization. Please simplify the query.
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
at System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()


I am using an older ORM that dymanically creates an update statement. Here is a essentially what my update statement looked like:

UPDATE Customer
SET FirstName=@FirstName,
LastName=@LastName,
CustomerId=@CustomerId
VersionNumber=@VersionNumber + 1
WHERE CustomerId = @CustomerId
and VersionNumber = @VersionNumber

The bolded line is setting the primary key field, CustomerId, to the same value that exists in the database. When I pulled the line of code out of the update statement, the problem went away.

I've always known that updating the primary key in a record is a bad idea, but setting it to the same value seems pretty harmless. My guess is that a new update of SQL 2005 broke things.

Here is a KB article on the error: http://support.microsoft.com/default.aspx/kb/945896

Tuesday, November 4, 2008

Checking for Multiple Instances of a Windows Forms or Console Application

When running a windows application or console/batch application you may run into issues if you have multiple instances running on the same machine. The applications may cause contention when reading and writing to a file system or database. Depending on how you design your application, it may cause data loss.

To handle this, you can use the System.Diagnostics namespace to check for existing instances of the application. If an instance exists with a different process ID, kill the existing process.

Note: You can kill the existing instance instead using process.Kill();

// Grab the current process so you can pull it's name
Process currentProcess = Process.GetCurrentProcess();
// Get existing processes on the current machine with the same name
Process[] processes = Process.GetProcessesByName(currentProcess.ProcessName);
foreach (Process process in processes)
{
// Loop through and check for any instance with the same name
if (process.Id != currentProcess.Id)
{
MessageBox.Show("Application is already running");
Application.Exit();
return;
}
}

// This piece of code isn't necessarily required. When using Visual Studio, your windows
// app runs under [ApplicationNam].vshost. This checks for these processes as well.
processes = Process.GetProcessesByName(currentProcess.ProcessName.Replace(".vshost", ""));
foreach (Process process in processes)
{
if (process.Id != currentProcess.Id)
{
MessageBox.Show("Application is already running");
Application.Exit();
return;
}
}