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;
}