AJAX, Jscript and Microsoft CRM

Hello JScript friends,

I'm back from vacation and back again to enjoy some interesting jscript stuff with you. Do you like JScript? I do not! However, since Web 2.0 and AJAX, it has become very popular because it makes web applications much more user friendly. And you could use jscript even for extending the client side behaviour of Microsoft Dynamics CRM. That is one more reason to use the good old trial-and-error script language. Especially AJAX techniques will help you to add validation logic on the client side of Microsoft CRM.

So what is AJAX? The magic word AJAX (Asynchronous Javascript and XML) is a web development technique for creating rich and interactive web applications, where additional data is loaded and displayed after the page is already loaded in the browser. The motivation for this technique is to make web apps more user friendly by minimizing the numbers of manual page loads and roundtrips between server and client.  AJAX has become a buzz word in 2005 but the technique is not very new. The first time I saw it was in the Navision User Portal years ago, a web based application to access the Navision ERP-System.

In this post I would like to show you how you could use AJAX-techniques together with Microsoft CRM to implement the following functionality:

1. Creating auto suggestion fields for Microsoft CRM
2. Load the data for the auto suggestion list via a web service.

Just take a look at the screenshots and you will understand what I mean. This is just a simple functionality/function but you can use this technique in many other scenarios. E.g. server side validations without a roundtrip or having a central list without using picklists.

1. Creating auto suggestion fields for Microsoft CRM

Lets start: just copy the following function into the onload event of any entity.

// Function for adding suggestion functionality
// for text input fields in Microsoft CRM
// textfield: document.getElementById('address1_country')
// method: a function, that returns an array of strings
// preload: true or false
function SuggestionTextBox(textfield, method, preload)
{   // max items in the suggestion box
    var maxItems = 6;

this.suggestionList = new Array();
this.suggestionListDisplayed = new Array();

var actual_textfield = textfield;
var actual_value = '';

var selectedNumber = 0;
var countMatches = 0;

if (preload)
{
        // load the data via external method
        this.suggestionList = method();
    }

    // attach this function to the textfield
    textfield.attachEvent("onfocus", initTextfield);

    // init textfield and attach necessary events
    function initTextfield()
{
        // when leaving the field we have to clear our site
        textfield.attachEvent("onblur", resetTextfield);
document.attachEvent("onkeydown", keyDown);
    }

    function resetTextfield(e)
{
        //when leaving the field, we have to remove all attached events
        document.detachEvent("onkeydown", keyDown);
        textfield.detachEvent("onblur",resetTextfield);
    }

    function keyDown(e)
{
keyCode = e.keyCode;

switch (keyCode)
{
            case 9: case 13:
                // enter & tab key
                if (countMatches > 0)
{

actual_textfield.value = suggestionListDisplayed[selectedNumber];

if (document.getElementById('suggestion_table') != null)
{
                       document.body.removeChild(document.getElementById('suggestion_table'));
                    }

}

                break;
case 38:
                //pressing up key
                if(selectedNumber > 0 && countMatches > 0)
{
selectedNumber--;
                    createSuggestionTable();
                }

                return false;
break;
case 40:
                // pressing down key
                if(selectedNumber < countMatches-1 && countMatches > 0 && selectedNumber < maxItems)
{
selectedNumber++;
                       createSuggestionTable();
                }

                return false;
break;
default:
                // do not call the function to often
                setTimeout(
                            function()
{
executeSuggestion(keyCode)
},  200 /* in ms */
                           );
break;
        }
}

    function executeSuggestion(keyCode)
{
selectedNumber = 0;
        countMatches = 0;

        actual_value = textfield.value;
        //todo add keyCode

// get all possible values from the suggestionList

        if (!preload)
{
            // load the data via external method
// todo add some caching function
            this.suggestionList = method();
        }

        // using regular expressions to match it against the suggestion list
        var re = new RegExp(actual_value, "i");

        //if you want to search only from the beginning
//var re = new RegExp("^" + actual_value, "i");

        countMatches = 0;
this.suggestionListDisplayed = new Array();

        // test each item against the RE pattern
        for (i = 0; i < this.suggestionList.length; i++)
{
            // if it matche add it to suggestionListDisplayed array
            if (re.test(this.suggestionList[i]) && actual_value != '')
{
                this.suggestionListDisplayed[countMatches] = this.suggestionList[i];
                countMatches++;

                // if there are more values than in maxItems, just break
                if (maxItems == countMatches)
                    break;
            }
}

        if (countMatches > 0)
{
createSuggestionTable();
        }
        else
        {
            if (document.getElementById('suggestion_table'))
{
                document.body.removeChild(document.getElementById('suggestion_table'));
            }
}
}

    function createSuggestionTable()
{

        if (document.getElementById('suggestion_table'))
{
            document.body.removeChild(document.getElementById('suggestion_table'));
        }

        // creating a table object which holds the suggesions
        table = document.createElement('table');
        table.id = 'suggestion_table';

        table.width = actual_textfield.style.width;
        table.style.position= 'absolute';
        table.style.zIndex = '100000';

        table.cellSpacing = '1px';
        table.cellPadding = '2px';

        topValue = 0;
        objTop = actual_textfield;
while(objTop)
{
topValue += objTop.offsetTop;
            objTop = objTop.offsetParent;
        }

table.style.top = eval(topValue + actual_textfield.offsetHeight) + "px";

        leftValue = 0;
        objLeft = actual_textfield
        while(objLeft)
{
leftValue += objLeft.offsetLeft;
            objLeft = objLeft.offsetParent;
        }

table.style.left = leftValue + "px";

        table.style.backgroundColor = '#FFFFFF';
        table.style.border = "solid 1px #7F9DB9";
        table.style.borderTop = "none";

document.body.appendChild(table);

        // iterate list to create the table rows
        for ( i = 0; i < this.suggestionListDisplayed.length; i++)
{
row = table.insertRow(-1);

                row.id = 'suggestion_row' + (i);
                column = row.insertCell(-1);
                column.id = 'suggestion_column' + (i);

if (selectedNumber == i)
{
column.style.color = '#ffffff';
                    column.style.backgroundColor = '#316AC5';
                }
                else
                {
column.style.color = '#000000';
                    column.style.backgroundColor = '#ffffff';
                }

column.style.fontFamily = 'Tahoma';
                column.style.fontSize = '11px';
                column.innerHTML = this.suggestionListDisplayed[i];

        }
}

    // return object
    return this;
}

Next, we have to call the function for every textfield where we want to add this behavior.

var f = function ListOfCountries()
{
        return new Array('Afghanistan','Albania','Algeria','American Samoa','Andorra','Angola','Anguilla','Antigua and Barbuda','Argentina','Armenia','Aruba','Australia','Austria','Azerbaijan','Bahamas','Bahrain','Bangladesh','Barbados','Belarus','Belgium','Belize','Benin','Bermuda','Bhutan','Bolivia','Bosnia-Herzegovina','Botswana','Bouvet Island','Brazil','Brunei','Bulgaria','Burkina Faso','Burundi','Cambodia','Cameroon','Canada','Cape Verde','Cayman Islands','Central African Republic','Chad','Chile','China','Christmas Island','Cocos (Keeling) Islands','Colombia','Comoros','Conch Republic','Congo, Democratic Republic of the (Zaire)','Congo, Republic of','Cook Islands','Costa Rica','Croatia','Cuba','Cyprus','Czech Republic','Denmark','Djibouti','Dominica','Dominican Republic','Ecuador','Egypt','El Salvador','Equatorial Guinea','Eritrea','Estonia','Ethiopia','Falkland Islands','Faroe Islands','Fiji','Finland','France','French Guiana','Gabon','Gambia','Georgia','Germany','Ghana','Gibraltar','Greece','Greenland','Grenada','Guadeloupe (French)','Guam (USA)','Guatemala','Guinea','Guinea Bissau','Guyana','Haiti','Holy See','Honduras','Hong Kong','Hungary','Iceland','India','Indonesia','Iran','Iraq','Ireland','Israel','Italy','Ivory Coast (Cote D`Ivoire)','Jamaica','Japan','Jordan','Kazakhstan','Kenya','Kiribati','Kuwait','Kyrgyzstan','Laos','Latvia','Lebanon','Lesotho','Liberia','Libya','Liechtenstein','Lithuania','Luxembourg','Macau','Macedonia','Madagascar','Malawi','Malaysia','Maldives','Mali','Malta','Marshall Islands','Martinique (French)','Mauritania','Mauritius','Mayotte','Mexico','Micronesia','Moldova','Monaco','Mongolia','Montserrat','Morocco','Mozambique','Myanmar','Namibia','Nauru','Nepal','Netherlands','Netherlands Antilles','New Caledonia (French)','New Zealand','Nicaragua','Niger','Nigeria','Niue','Norfolk Island','North Korea','Northern Mariana Islands','Norway','Oman','Pakistan','Palau','Panama','Papua New Guinea','Paraguay','Peru','Philippines','Pitcairn Island','Poland','Polynesia (French)','Portugal','Puerto Rico','Qatar','Reunion','Romania','Russia','Rwanda','Saint Helena','Saint Kitts and Nevis','Saint Lucia','Saint Pierre and Miquelon','Saint Vincent and Grenadines','Samoa','San Marino','Sao Tome and Principe','Saudi Arabia','Schengen countries','Senegal','Serbia and Montenegro','Seychelles','Sierra Leone','Singapore','Slovakia','Slovenia','Solomon Islands','Somalia','South Africa','South Korea','Spain','Sri Lanka','Sudan','Suriname','Svalbard and Jan Mayen Islands','Swaziland','Sweden','Switzerland','Syria','Taiwan','Tajikistan','Tanzania','Thailand','Timor-Leste (East Timor)','Togo','Tokelau','Tonga','Trinidad and Tobago','Tunisia','Turkey','Turkmenistan','Turks and Caicos Islands','Tuvalu','Uganda','Ukraine','United Arab Emirates','United Kingdom','United States','Uruguay','Uzbekistan','Vanuatu','Venezuela','Vietnam','Virgin Islands','Wallis and Futuna Islands','Yemen','Zambia','Zimbabwe');
    }

var obj = SuggestionTextBox(document.getElementById('address1_country'), f, true);

 

We will call the function "SuggestionTextBox" and passing three parameter:

  1. The textbox we would like to extend in this case 'address1_country'
  2. A function which returns an array of strings, in this case f, which returns a list of countries
  3. The last parameter controls if the list of values will be loaded just one time or if false, everytime the user is pressing a key. So for long lists the second option might be better. So if true, the list should contain all possible values and if false it should contain only the result for the given input of the textbox.

For demoing purpuses I just created the function 'f', which returns some countries. So copying the above code into the onload event handler of the entity. Thats it, just save your form and publish your changes. Now you can open the form and by typing the first character the auto suggestion box will appear.

2. Load the data for the auto suggestion list via a web service.

Now that we have a great suggestion functionality we can now start to make the list a little bit more dynamic. So instead of fixed function which returns static list of items, we could load them every time by using a web service.

Web Service function, which returns a list of values:

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;

[WebService(Namespace = "http://webservices/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService
{
    public Service () {

        //Uncomment the following line if using designed components
//InitializeComponent();
    }

[WebMethod]
    public string GetListOfCountries()
{
      // it might be a good idea to get the values out of a database or from Microsoft CRM
        string[] arrayCountries = new string[] { "Afghanistan","Albania","Algeria","..."};
return string.Join(",,", arrayCountries);
    }
}

Last step, we will call the web service and init our suggestion text box. Add the following code to your onload event handler:

var countries = new Array();
var obj;

var f = function GetListOfCountries()
{
    return countries;
}

function CallWebService()
{

    var objHttp;

    // create an XmlHttp instance
    objHttp = new ActiveXObject("Microsoft.XMLHTTP");

    // Create the SOAP Envelope
    strEnvelope = "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" +
            " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"" +
            " xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
            " <soap:Body>" +
            " <" + "GetListOfCountries" + " xmlns=\"http://webservices/\">" +
            " </" + "GetListOfCountries" + ">" +
            " </soap:Body>" +
            "</soap:Envelope>";

    //<?xml version="1.0" encoding="utf-8"?>
//<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
// <soap:Body>
// <GetListOfCities xmlns="http://webservices/" />
// </soap:Body>
//</soap:Envelope>

// Set up the post
    objHttp.onreadystatechange = function()
{
        // a readyState of 4 means we're ready to use the data returned by XMLHTTP
        if (objHttp.readyState == 4)
{
            // get the return envelope
            var szResponse = objHttp.responseText;

var startTag = "<string xmlns=\"http://webservices/\">";
var endTag = "</string>";
var cities;

var valueStart = 0;
var valueEnd = 0;

            //Parsing the returned XML
            valueStart = objHttp.responseXML.xml.indexOf(startTag, valueEnd) + startTag.length;
            valueEnd = objHttp.responseXml.xml.indexOf(endTag, valueEnd+1);

var tmpCountries = objHttp.responseXML.xml.substring(valueStart, valueEnd);

            countries = tmpCountries.split(',,');

            obj = SuggestionTextBox(document.getElementById('address1_country'), f, true);

        }
}

    var szUrl;
    szUrl = "http://website/WebServices/Service.asmx/GetListOfCountries";

    // send the POST to the Web service
    objHttp.open("POST", szUrl, true);
    objHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    objHttp.send(strEnvelope);

}

CallWebService();

Thats it. Do you like it? Please let me know if that works fine for you or if you made any improvements. I'd love to hear from you!

Other interesting blogs regarding this topic:

Accessing Web Services From CRM Forms
http://blogs.msdn.com/arash/archive/2006/02/01/521565.aspx

Address Validation in Three Easy Steps!
http://www.invokesystems.com/cs/blogs/mscrm/archive/2006/06/21/38.aspx

Working with CRM Form Lookup Controls Programmatically
http://blogs.msdn.com/arash/archive/2006/03/28/562846.aspx

Finally there: Show and hide fields based on the users role!
http://ronaldlemmen.blogspot.com/2006/05/finally-there-show-and-hide-fields.html

Inserting City and State Automatically based upon Zip Code
http://blogs.msdn.com/midatlanticcrm/archive/2006/06/22/Inserting_City_and_State_Automatically_based_upon_Zip_Code_Microsoft_CRM_3.aspx

 

auto_complete_suggestion.gif