Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

Sunday, August 16, 2009

Using Unresolved Email Recipients in CRM 4.0

Unresolved emails are useful in that you can send an email to someone without having them setup as a system record (contact, account, lead, queue, user, ect).  Unresolved email addresses are turned off by default.  They should be used with caution.  If you send an email to an unresolved address it will not track the email to that lead/contact since one does not exist.  It also does not leverage the email "Do not allow" flags to allow people to opt out of emails.  With those considerations, there are still cases where it is very useful to send an email to someone without having to create a contact record.



I will show two example of sending emails to unresolved recipients.  The first will be sent from the email form using JavaScript.  The second will be done in a plugin.  We will add two unresolved email addresses as CC addresses on emails sent from a contact record.


Before we begin.  You must flip the setting to allow unresolved recipients.  Go to Settings --> Administration --> System Settings --> Email Tab.  Set Allow messages with unresolved e-mail recipients to be sent to yes.




 Second, we will add a couple of fields to the contact for secondary contacts email addresses.  These fields will contain our unresolved email addresses.




JavaScript Implementation
When the user clicks "Send Email" from the contact form we want the CC field to default with the two unresolved email addresses.  To do this we add the following Jscript to the onload of the email form.  The code will check to see if the regarding object is of type contact since this form is used for all emails.  Then it adds each email address to an email object that follows the activityparty schema for unresolved email addresses.  It has a type of 9206 and the email is set in the data property.

if (crmForm.FormType == 1 &&
    crmForm.all.regardingobjectid.DataValue != null &&
    crmForm.all.regardingobjectid.DataValue[0].typename == "contact" &&
    window.opener != null &&
    window.opener.document != null) {



    var ar = new Array();
    var emailObj = new Object();


    if (window.opener.document.crmForm.all.new_secondarycontactemail1 != null &&
            window.opener.document.crmForm.all.new_secondarycontactemail1.DataValue != null) {
        emailObj = new Object();
        emailObj['type'] = '9206';
        emailObj['category'] = '3';
        emailObj['data'] = window.opener.document.crmForm.all.new_secondarycontactemail1.DataValue;
        emailObj['name'] = window.opener.document.crmForm.all.new_secondarycontactemail1.DataValue;
        ar.push(emailObj);
    }



    if (window.opener.document.crmForm.all.new_secondarycontactemail2 != null &&
            window.opener.document.crmForm.all.new_secondarycontactemail2.DataValue != null) {
        emailObj = new Object();
        emailObj['type'] = '9206';
        emailObj['category'] = '3';
        emailObj['data'] = window.opener.document.crmForm.all.new_secondarycontactemail2.DataValue;
        emailObj['name'] = window.opener.document.crmForm.all.new_secondarycontactemail2.DataValue;
        ar.push(emailObj);
    }



    crmForm.all.cc.DataValue = ar;


}


Lastly, publish the customization.  When you click "Send Email" from the contact form your addresses will be added to the CC. 


 


 


Plugin Implementation


The same functionality can be added using a plugin.  The benefits of using a plugin versus a client side implementation is that you can implement consistent functionality regardless of whether the email send is triggered from a client portal, another plugin, or through the CRM UI.  The JavaScript implemenation is useful in that the user can see who the email is being sent to before they click Send. 



Install the plugin using the registration tool as a pre-create step for email save.  This will add the CC on the initial save of the email.  It is triggered regardless of whether the user clicks save or send from the email form.  The code also includes duplicate checking logic. So, if the user adds the email address to the CC from the form it will not duplicate the same address in the plugin.


using System.Web;
using System.Net;


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;
using System.Reflection;
using System.Web.Services.Protocols;
using Microsoft.Crm.SdkTypeProxy.Metadata;
using Microsoft.Crm.Sdk.Metadata;
using Inetium.CrmPlugins;
using System.Collections;



namespace Inetium.CrmPlugins.Plugins
{
    public class CaseUnresolvedEmailPlugin : IPlugin
    {
        public void Execute(IPluginExecutionContext context)
        {
            ICrmService service = null;
            try
            {
                if (context.InputParameters.Properties.Contains("Target") &&
                    context.InputParameters.Properties["Target"] is DynamicEntity)
                {
                    DynamicEntity entityInput = context.InputParameters.Properties["Target"] as DynamicEntity;
                    if (entityInput.Properties.Contains("regardingobjectid"))
                    {
                        Lookup regardingObjectLookup = entityInput.Properties["regardingobjectid"] as Lookup;
                        service = context.CreateCrmService(true);
                       
                        // Only using this for contacts
                        if (regardingObjectLookup.type == EntityName.contact.ToString())
                        {
                            // Pull parent contact - using helper method
                            DynamicEntity ctn = CRMUtilities.RetrieveById(service, EntityName.contact.ToString(), "contactid", regardingObjectLookup.Value, new ColumnSet(new string[]{"new_secondarycontactemail1", "new_secondarycontactemail2"}));
                            if (!ctn.Properties.Contains("new_secondarycontactemail1") && !ctn.Properties.Contains("new_secondarycontactemail2"))
                                return;



                            string email1 = ctn.Properties.Contains("new_secondarycontactemail1") ? ctn.Properties["new_secondarycontactemail1"].ToString() : "";
                            string email2 = ctn.Properties.Contains("new_secondarycontactemail2") ? ctn.Properties["new_secondarycontactemail2"].ToString() : "";


                            DynamicEntity[] partyArrayExisting = entityInput.Properties.Contains("cc") ? entityInput.Properties["cc"] as DynamicEntity[] : new DynamicEntity[] { };
                            List<DynamicEntity> partyList = new List<DynamicEntity>(partyArrayExisting);



                            if (email1 != "" && !IsEmailAlreadyInCC(email1, partyList))
                            {
                                DynamicEntity party = new DynamicEntity();
                                party.Name = EntityName.activityparty.ToString();
                                party.Properties["addressused"] = email1;
                                partyList.Add(party);
                            }



                            if (email2 != "" && !IsEmailAlreadyInCC(email2, partyList))
                            {
                                DynamicEntity party = new DynamicEntity();
                                party.Name = EntityName.activityparty.ToString();
                                party.Properties["addressused"] = email2;
                                partyList.Add(party);
                            }



                            // Generate CC's list by adding in those from contacts
                            if (partyList.Count > 0)
                                entityInput.Properties["cc"] = partyList.ToArray();


                        }


                    }
                }
            }
            catch (SoapException se)
            {
                throw new Exception(se.Detail.InnerText);
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
            finally
            {
                if (service != null)
                {
                    service.Dispose();
                    service = null;
                }
            }
        }



        private bool IsEmailAlreadyInCC(string email, List<DynamicEntity> activityPartyList)
        {
            bool bAlreadyExists = false;
            foreach (DynamicEntity existingParty in activityPartyList)
            {
                // compare to existing CCs
                if (existingParty.Properties.Contains("addressused") &&
                    (existingParty.Properties["addressused"].ToString() == email))
                {
                    bAlreadyExists = true;
                    break;
                }
            }
            return bAlreadyExists;
        }
    }
}



Dynamics CRM 4.0 - JavaScript Web Service Helper Objects Part II

In February I posted a helper library for making JavaScript call to CRM - Original Post. Since then I have made a few enhancements.




  • Adding ordering to your results

  • Allowing for Null/Not Null Checks


The below example is used to default the address on a new contact to an address for their parent account. It pulls the first address (ordering by address number) where the address line 1 is not null (not null check).


try {
if (crmForm.all.parentaccountid.DataValue != null) {
// Create object passing in the entity you are selecting from
var crmService = new CrmService("customeraddress", LOGICAL_OPERATOR_AND);
crmService.AddColumn("name");
crmService.AddColumn("line1");
crmService.AddColumn("line2");
crmService.AddColumn("line3");
crmService.AddColumn("city");
crmService.AddColumn("stateorprovince");
crmService.AddColumn("postalcode");
crmService.AddColumn("country");

// Order by address number ascending
crmService.AddOrder("addressnumber", "Ascending");



// Add filter conditions (note: the "AND" logical operator was specified in constructor)
crmService.AddFilterCondition("parentid", crmForm.all.parentaccountid.DataValue[0].id, CONDITION_OPERATOR_EQUAL);

// Where line1 is not null
crmService.AddFilterCondition("line1", "", "NotNull");



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


// Loop through rows and select values (they return strings)
if (result.Rows.length > 0) {
var row = result.Rows[0];
// Get Column By Name
crmForm.all.address1_name.DataValue = row.GetValue("name");
crmForm.all.address1_line1.DataValue = row.GetValue("line1");
crmForm.all.address1_line2.DataValue = row.GetValue("line2");
crmForm.all.address1_line3.DataValue = row.GetValue("line3");
crmForm.all.address1_city.DataValue = row.GetValue("city");
crmForm.all.address1_stateorprovince.DataValue = row.GetValue("stateorprovince");
crmForm.all.address1_postalcode.DataValue = row.GetValue("postalcode");
crmForm.all.address1_country.DataValue = row.GetValue("country");



}
}
}
catch (e) {
alert(e.message);


}


The new library is attached below. Enjoy!


-Andrew



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



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.

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