Sunday, September 27, 2009

CRM JavaScript Web Service Helper

[UPDATE - Jan 21, 2010] I have released an updated version of script under the name of CRM Web Service Toolkit, please check my latest blog for the details.

In CRM's form customization, we often need to implement business logics based on the information that is not available right away from the crmForm object. In this case, it's common practice to use CRM Web Service to retrieve this type of information from CRM database. This is what I have been doing extensively in my current CRM project, so I spent some of my weekend time to create a re-usable JavaScript CRM Web Service Helper, which you can copy and use.
CrmServiceHelper = function()
{
    /**
     * CrmServiceHelper 1.0
     *
     * @author Daniel Cai
     * @website http://danielcai.blogspot.com/
     * @copyright Daniel Cai
     * @license Microsoft Public License (Ms-PL), http://www.opensource.org/licenses/ms-pl.html
     *
     * This release is provided "AS IS" and contains no warranty or whatsoever.
     *
     * Date: Sep 27 2009
     */

    // Private members
    var DoRequest = function(soapBody, requestType)
    {
        //Wrap the Soap Body in a soap:Envelope.
        var soapXml =
                "<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'>" +
                GenerateAuthenticationHeader() +
                "<soap:Body><" + requestType + " xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>" +
                soapBody + "</" + requestType + ">" +
                "</soap:Body>" +
                "</soap:Envelope>";

        var xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
        xmlhttp.open("POST", "/MSCRMServices/2007/crmservice.asmx", false);
        xmlhttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
        xmlhttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/" + requestType);

        //Send the XMLHTTP object.
        xmlhttp.send(soapXml);

        var resultXml = xmlhttp.responseXML;

        if (resultXml === null || resultXml.xml === null || resultXml.xml === "")
        {
            if (xmlhttp.responseText !== null && xmlhttp.responseText !== "")
            {
                throw new Error(xmlhttp.responseText);
            }
            else
            {
                throw new Error("No response received from the server. ");
            }
        }

        // Report the error if occurred
        var error = resultXml.selectSingleNode("//error");
        var faultString = resultXml.selectSingleNode("//faultstring");

        if (error !== null || faultString !== null)
        {
            throw new Error(error !== null ? resultXml.selectSingleNode('//description').nodeTypedValue : faultString.text);
        }

        return resultXml;
    };

    var BusinessEntity = function(sName)
    {
        this.name = sName;
        this.attributes = new Object();
    };

    var DataType = {
        String : "string",
        Boolean : "boolean",
        Int : "int",
        Float : "float",
        DateTime : "datetime"
    };

    // Public members
    return {
        BusinessEntity : BusinessEntity,

        DataType : DataType,

        DoRequest : DoRequest,

        Retrieve : function(entityName, id, columns)
        {
            var attributes = "";
            if (typeof attributes !== "undefined")
            {
                for (var i = 0; i < columns.length; i++)
                {
                    attributes += "<q1:Attribute>" + columns[i] + "</q1:Attribute>";
                }
            }

            var msgBody =
                    "<entityName>" + entityName + "</entityName>" +
                    "<id>" + id + "</id>" +
                    "<columnSet xmlns:q1='http://schemas.microsoft.com/crm/2006/Query' xsi:type='q1:ColumnSet'>" +
                    "<q1:Attributes>" +
                    attributes +
                    "</q1:Attributes>" +
                    "</columnSet>";

            var resultXml = DoRequest(msgBody, "Retrieve");
            var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
            xmlDoc.async = false;
            xmlDoc.loadXML(resultXml.xml);

            var retrieveResult = xmlDoc.selectSingleNode("//RetrieveResult");
            if (retrieveResult === null)
            {
                throw new Error("Invalid result returned from server. ");
            }

            var resultNodes = retrieveResult.childNodes;
            var returnEntity = new BusinessEntity();
            for (var i = 0; i < resultNodes.length; i++)
            {
                var fieldNode = resultNodes[i];
                var field = {};
                field["value"] = fieldNode.text;

                for (var j = 0; j < fieldNode.attributes.length; j++)
                {
                    field[fieldNode.attributes[j].nodeName] = fieldNode.attributes[j].nodeValue;
                }

                returnEntity.attributes[fieldNode.baseName] = field;
            }

            return returnEntity;
        },

        Fetch : function(xml)
        {
            var msgBody = "<fetchXml>" + _HtmlEncode(xml) + "</fetchXml>";

            var resultXml = DoRequest(msgBody, "Fetch");
            var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
            xmlDoc.async = false;
            xmlDoc.loadXML(resultXml.xml);

            var fetchResult = xmlDoc.selectSingleNode("//FetchResult");
            if (fetchResult === null)
            {
                throw new Error("Invalid result returned from server. ");
            }
            xmlDoc.loadXML(fetchResult.childNodes[0].nodeValue);

            var resultNodes = xmlDoc.selectNodes("/resultset/result");
            var results = [];

            for (var i = 0; i < resultNodes.length; i++)
            {
                var resultEntity = new BusinessEntity();

                for (var j = 0; j < resultNodes[i].childNodes.length; j++)
                {
                    var fieldNode = resultNodes[i].childNodes[j];
                    var field = {};
                    field["value"] = fieldNode.text;

                    for (var k = 0; k < fieldNode.attributes.length; k++)
                    {
                        field[fieldNode.attributes[k].nodeName] = fieldNode.attributes[k].nodeValue;
                    }

                    resultEntity.attributes[fieldNode.baseName] = field;
                }

                results[i] = resultEntity;
            }

            return results;
        },

        Execute : function(request)
        {
            var msgBody = request;

            var resultXml = DoRequest(msgBody, "Execute");
            var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
            xmlDoc.async = false;
            xmlDoc.loadXML(resultXml.xml);
            return xmlDoc;
        },

        ParseValue : function(businessEntity, crmProperty, type, crmPropertyAttribute)
        {
            if (businessEntity === null || typeof crmProperty === "undefined" || !businessEntity.attributes.hasOwnProperty(crmProperty))
            {
                return null;
            }

            var value = (typeof crmPropertyAttribute !== "undefined")
                    ? businessEntity.attributes[crmProperty][crmPropertyAttribute]
                    : businessEntity.attributes[crmProperty].value;

            switch (type)
                    {
                case DataType.Boolean:
                    return (value !== null) ? (value === "1") : false;
                case DataType.Float:
                    return (value !== null) ? parseFloat(value) : 0;
                case DataType.Int:
                    return (value !== null) ? parseInt(value) : 0;
                case DataType.DateTime:
                    return (value !== null) ? ParseDate(value) : null;
                case DataType.String:
                    return (value !== null) ? value : "";
                default:
                    return (value !== null) ? value : null;
            }

            return null;
        }
    };
}();

The following are a few scenarios that your might find the CRM Web Service Helper userful.
  1. Fetch a list of records or one record, in which case you can use CrmServiceHelper.DoFetchXmlRequest().
    var fetchXml = 
    '<fetch mapping="logical">' +
       '<entity name="account">' +
          '<attribute name="name" />' +
          '<attribute name="primarycontactid" />' +
          '<filter>' +
             '<condition attribute="accountid" operator="eq" value="' + crmForm.all.accountid.DataValue[0].id + '" />' +
          '</filter>' +
       '</entity>' +
    '</fetch>';  
     
    var fetchResult = CrmServiceHelper.Fetch(fetchXml);
    alert(CrmServiceHelper.ParseValue(fetchResult[0], 'name'));
    

  2. Retrieve one record.
    var retrieveResult = CrmServiceHelper.Retrieve('account', crmForm.all.accountid.DataValue[0].id, ['accountid', 'name']);
    alert(CrmServiceHelper.ParseValue(retrieveResult, 'name'));

  3. Execute a request.
    function GetCurrentUserId()
    {
       var request = "<Request xsi:type='WhoAmIRequest' />";
       var xmlDoc = CrmServiceHelper.Execute(request);
     
       var userid = xmlDoc.getElementsByTagName("UserId")[0].childNodes[0].nodeValue;
       return userid;
    }

Beyond the above example, you can use CrmServiceHelper.DoRequest() function to make any other CRM service calls including Create, Update, Delete, etc.

A few final notes about Web Service Helper:
  1. CrmServiceHelper is designed as a container object to provide all necessary interfaces to interact with CRM Web Service through JavaScript. By this approach, we don't pollute JavaScript global namespace with a lot of objects.
  2. All functions in CrmServiceHelper throws an error when exception happens, it's your responsibility to handle this type of exception. The common practice is using try/catch block, but it's totally up to you. If you don't use try/catch block, CRM platform will catch it, and give the user an alert warning window.
  3. You should probably consider saving the above script to a file such as CrmServiceHelper.js, and upload the file to your CRM's ISV folder, then use Henry Cordes' load_script() function to load and consume the service helper in the form's onload event.
  4. When you use CrmServiceHelper.GetValueFromFetchResult() function to parse the fetched result, please make sure to specify the datatype correctly.
  5. FetchXML is extremely flexible, it can do almost anything that you may want (except some native SQL functions such as SOUNDEX, etc.) in CRM, FetchXML is usually my first choice when I need to retrieve more than one record from CRM using JavaScript (In C#, you might want to use Query Expression due to the syntax friendship in IDE environment). You might want to consider using Stunnware Tools to help you create FetchXML in a more productive way.

[UPDATE - Jan 21, 2010] I have released an updated version of script under the name of CRM Web Service Toolkit, please check my latest blog for the details.

6 comments:

  1. Daniel, Thanks a lot for making these available! I'm using external files and including one for each of my entities in the onLoad event. So, I've made a separate file for your web service and I'm that in each of the onLoad scripts like this:

    //load CrmServiceHelper.js
    var script2Load = document.createElement("SCRIPT");
    script2Load.language = "javascript";
    script2Load.src = "/ISV/" + ORG_UNIQUE_NAME + "/" + "CrmServiceHelper.js?nocach=" + Math.random();
    document.getElementsByTagName("HEAD")[0].appendChild(script2Load);
    script2Load.onreadystatechange = OnScriptReadyState;

    function OnScriptReadyState() {

    if (event.srcElement.readyState == "loaded") {

    //DOING STUFF HERE

    }
    }

    Do you think this is OK? The reason I'm doing this is because I'm hoping to use the Javascript Factory from Stunnware one day, so I'm trying to keep everything in one external file per form.

    ReplyDelete
  2. Hi Nathan,

    I think the way you load the external script looks fine. But frankly I am not a big fan of this approach, as you may find yourself in a situation that you need to load more than one JS files some time, in which case you need to monitor all script files being loaded before doing anything on the form. I have documented a different approach at http://danielcai.blogspot.com/2009/10/crm-40-convert-country-provincestate.html (look all the way down), by which you can call the LoadExternalScript(myscriptfile) function, and then you are good to make CRM service calls. This particular approach makes uses of JavaScript eval() function, which could bring up some debate whether it's the best practice. But I just liked the simplicity.

    Cheers.

    ReplyDelete
  3. Daniel,

    See the comments at the end of this posting for how I did it in the end:
    http://kiavashshakibaee.blogspot.com/2009/02/reference-external-javascript-file-in.html

    By the way do you do freelance work?

    ReplyDelete
  4. Hi Nathan,

    Glad to know that you have made it.

    I am afraid that I may not be able to commit to freelance work at this time. But if you need some advice or there is something interesting, you can always drop me an email at danielwcai at gmail.com.

    BTW, I have done some important refactoring during the weekend, so this helper utility now supports all important CRM Web Service messages, including Create, Update, Delete, Retrieve, RetriveMultiple, Execute. Also it has now test cases included, that can be used as sample code. I am trying to finish the document, have planned to make it avaiable on codeplex site within this week.

    Stay tuned.

    Cheers.

    ReplyDelete
  5. Thanks a lot Daniel. Your blog is on my list of RSS feeds and I read it as soon as it changes everytime!

    Looking forward to the new stuff!

    ReplyDelete
  6. Hi,
    the line 192 causes an error if you try to fetch an non existing error.

    Your line:
    if (businessEntity === null || typeof crmProperty === "undefined" || !businessEntity.attributes.hasOwnProperty(crmProperty))

    Corrected:
    if (businessEntity === null ||typeof businessEntity === "undefined" || typeof crmProperty === "undefined" || !businessEntity.attributes.hasOwnProperty(crmProperty))


    Best regards

    Sven Rosenberger

    ReplyDelete