Tuesday, January 13, 2009

AJAX, JScript and Microsoft CRM

I was looking around for a demo of AJAX and CRM and found this article on Microsoft Dynamics CRM @ Joris Kalz's WebLog. The Full article with interesting comments can be found here.


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

No comments: