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

Wednesday, October 15, 2008

Page does not contain a definition for 'Context'

I inherited a website this week and it wouln't build. In the past the website was built in debug mode and the files were moved out manually. It wouldn't built in release mode and it wouldn't allow for me to do a publish website. It received the error ... "does not contain a definition for context."

The web page had the following page definition:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="MyPage1.aspx.cs" Inherits="MyPage1" %>

Whereas, the codebehind for MyPage1 (MyPage1.aspx.cs) included the wrong class name. I updated the class name to correspond to the class name mentioned in the web page and it will now build in release mode.

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

public partial class MyPage2 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{

}
}

Sunday, October 5, 2008

Generating Insert Scripts to Move Static Data Between Environments

You may create a codes table that defines an order type or a sales code, ect. You probably create the table in your local database and manually enter in the initial rows. When it is time to move to test, stage, and production, you don't want to manually key in the data. You have a couple options. You can write an SSIS job, or do an export to file and re-import the data in the new environment. This just seemed like more work that necessary. I thought it would be really nice if I could have them documented as insert statements, so the installer can simply run it when it is time to go to production.

To handle this scenario, I found an excellent post from Narayana Vyas Kondreddi. All you need to do is install his stored procedure in your master database and then you can generate insert statements by calling that stored procedure in the database or your choosing.

http://vyaskn.tripod.com/code.htm#inserts

Here are the two main type of execute statements that I found useful.

  • Generating inserts for a table where you want all columns scripted:
EXEC sp_generate_inserts 'titles'

  • Generating a table to include all columns except for the identity column:
EXEC sp_generate_inserts mytable, @ommit_identity = 1


Thanks Narayana for a great post!!!