Friday, January 11, 2013

Create custom event handlers with CRM 2011

Introduction

CRM2011 provides the following out-of-the-box event handlers:

  • At a form level, OnLoad and OnSave
  • At a field level, OnChange.
As a seasoned event-driven developer, I find this depressing. I want more. For example, I want to be able to modify some other field as I type. Or, I want to execute complex validations involving multiple fields, without  cluttering up the form.

This article shows how to do that.

To understand the steps in this article it would be beneficial to first review this article: Create a general use Javascript library for CRM 2011

The concept

This solution revolves around the idea that you can register event handlers in Javascript.

Here is a very simple web page with an event handler:

<HTML>
  <BODY>
    <label id="Label1" onClick="DoTheThing()">Click me</label>
  <SCRIPT>
      function DoTheThing() {
          alert('thing was done');
      }
</SCRIPT>
</BODY>
</HTML>

However, in CRM I do not have access to the onClick event handler. But, I do have access to the CRM form's onLoad event; which fires when the form is loaded. This is where you will register the onClick event for the form to fire when the user clicks the form.

This is what it looks like with simple HTML; on load of the body the events were wired up dynamically. The advantage here is that I did not know need access to the label control by code, but I can "reach" it from the onload event of the body.


<HTML>
  <BODY onload="SetupEvents()">
    <label id="myLabel" >Click me</label>
  </BODY>
  <SCRIPT>
      function DoTheThing() {
          alert('thing was done');
      }
      function SetupEvents() {
          var label = document.getElementById('myLabel');
          label.attachEvent('onclick', DoTheThing);
      }
</SCRIPT>
</HTML>

So this is what will  be used in the CRM form - we can access the html field's events by injecting it into the OnLoad event of the form.

Script library

Firstly, add the following scripts as a general function library in the solution's web resources. Call it FcbGenericScripts.

//=======================================================================
//========================[ Generic library ]============================      //=======================================================================

      // Read a page's GET URL variables and return them as an associative array.
      // For example:
      // http://www.example.com/?me=myValue&name2=SomeOtherValue
      // Calling getUrlVars() function would return you the following array:
      // {   
      //             "me"    : "myValue",   
      //             "name2" : "SomeOtherValue"
      // }
      // To get a value of first parameter you would do this:
      //             var first = getUrlVars()["me"];
      // To get the second parameter:
      //             var second = getUrlVars()["name2"];

      function getUrlVars() {
          var vars = [], hash;
          var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
          for (var i = 0; i < hashes.length; i++) {
              hash = hashes[i].split('=');
              vars.push(hash[0]);
              vars[hash[0]] = hash[1];
          }
          return vars;
      }

      //=======================================================================
      // This receives the passed in context from the form, (done with the UI form designer, use the checkbox)
      // then interrogates the context to which entity member fired the event, then
      // extracts the value from that entity control,
      // strips out the formatting crap and makes sure it is a real number.
      // If the number is proper (10 numeric)  it formats the number back into a string
      // then writes the string back to the control.
      //If the number is not proper, it squeals at the user.
      //
      function FormatPhoneNumber(context) {
          var oField = context.getEventSource().getValue();
          var sAllNumeric = oField;

          if (typeof (oField) != "undefined" && oField != null) {
              sAllNumeric = oField.replace(/[^0-9]/g, "");
              switch (sAllNumeric.length) {
                  case 10:
                      sFormattedPhoneNumber = "(" + sAllNumeric.substr(0, 3) + ") " + sAllNumeric.substr(3, 3) + "-" + sAllNumeric.substr(6, 4)
                      break;

                  default:
                      alert("Phone must contain 10 numbers.")
                      break;
              }
          }
          context.getEventSource().setValue(sFormattedPhoneNumber);
      }

      //=======================================================================
      //This returns a reference to the control that is hosting the XRM entity object
      function getEntityFieldControl(sEntityFieldName, bShowErrorOnFail) {
          var returnValue = document.getElementById(sEntityFieldName);
          if (returnValue == null) {
              if (bShowErrorOnFail)
                  alert('Could not locate the html control named ' + sEntityFieldName);
              return null;
          }
          return returnValue;
      }

      //=======================================================================
      //This returns a reference to the XRM entity object itself
      function getEntityFieldObject(sEntityFieldName, bShowErrorOnFail) {
          var returnValue = Xrm.Page.getAttribute(sEntityFieldName)
          if (returnValue == null) {
              if (bShowErrorOnFail)
                  alert('Could not locate the entity object named ' + sEntityFieldName);
              return null;
          }
          return returnValue;
      }

      //=======================================================================
      //This returns the *value* of the XRM entity object's *control*
      //This is sometimes necessary, because the XRM DOM is not updated immediately as the field's contents are changing.
      function getEntityFieldControlValue(sEntityFieldName, bShowErrorOnFail) {
          var entityFieldObjectControl = getEntityFieldControl(sEntityFieldName, bShowErrorOnFail)
          if (entityFieldObjectControl == null) {
              return (null);
          }
          else {
              return entityFieldObjectControl.value;
          }
      }

      //=======================================================================
      //This returns the *value* of the XRM entity object
      function getEntityFieldValue(sEntityFieldName, bShowErrorOnFail) {
          var entityFieldObject = getEntityFieldObject(sEntityFieldName, bShowErrorOnFail)
          if (entityFieldObject == null) {
              return ("");
          }
          else {
              return entityFieldObject.getValue();
          }
      }

      //=======================================================================
      //This sets the *value* of the XRM entity object, which hopefully will update the control too.
      function setEntityFieldValue(sEntityFieldName, sNewValue, bShowErrorOnFail) {
          var entityFieldObject = getEntityFieldObject(sEntityFieldName, bShowErrorOnFail);
          if (entityFieldObject != null) {
              entityFieldObject.setValue(sNewValue);
          }
      }

      //=======================================================================
      //Sets up the control of an entity object to execute another javascript method when a specified JS event occurs to the control
      //i.e. "execute my javascript called ValidateEmail() method when the user modifies the email address. (onkeyup)
      function registerEntityFieldEventHandler(sEntityFieldName, sTriggeringEvent, oMethodToCall, bShowErrorOnFail) {
          var entityFieldControl = getEntityFieldControl(sEntityFieldName, bShowErrorOnFail)
          if (entityFieldControl != null) {
              entityFieldControl.attachEvent(sTriggeringEvent, oMethodToCall);
          }
      }

      fcbCustomJs =
{
    getUrlVars: function () {
        var vars = [], hash;
        var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
        for (var i = 0; i < hashes.length; i++) {
            hash = hashes[i].split('=');
            vars.push(hash[0]);
            vars[hash[0]] = hash[1];
        }
        return vars;
    },

    isEmpty: function (val) {
        if (val) {
            return ((val === null) || val.length == 0 || /^\s+$/.test(val));
        } else {
            return true;
        }
    },

    replaceData: function (s) {
        var arr1 = new Array('%20', '%21', '%40', '%23', '%24', '%25', '%5e', '%26', '%2a', '%28', '%29', '%2b', '%3d', '%2c', '%3c', '%3e', '%2f', '%3f', '%5b', '%5d', '%7b', '%7d', '%3a', '%7c');
        var arr2 = new Array(' ', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '=', ',', '<', '>', '/', '?', '[', ']', '{', '}', ':', '|');
        if (this.isEmpty(s)) return "";
        var re;
        for (var x = 0, i = arr1.length; x < i; x++) {
            re = new RegExp(arr1[x], 'g');
            s = s.replace(re, arr2[x]); //swap arr1 item with matching item from arr2   
        }
        return s;
    }
}         

Ok - all that library does is provide a javascript with the ability to interact with the XRM DOM field objects when you know the name of the CRM field. In addition, it provides a wrapper for attaching events to the XRM fields, when we know what the XRM field name is. Its just a library at this point, it does not do anything on its own. But, the next script will make use of it.

Now add another web resource, but this time this is not a generic library. It is specific to the entity I want to update, but it makes use of some of the functionality in the generic library we added previously. Call it jc_ServiceScripts. For the purpose of this example, assume there are 3 custom fields that are involved here:  fcb_rangelow, fcb_rangehigh and jc_defaultquantity
setupEvents = function () { registerEntityFieldEventHandler("fcb_rangelow", "onkeyup", doValidateValues, true) registerEntityFieldEventHandler("fcb_rangehigh", "onkeyup", doValidateValues, true) registerEntityFieldEventHandler("jc_defaultquantity", "onkeyup", doValidateValues, true) }; doValidateValues = function () { var currentDefaultValue = parseFloat(getEntityFieldControlValue("jc_defaultquantity", true)); var currentMinimumValue = parseFloat(getEntityFieldControlValue("fcb_rangelow", true)); var currentMaximumValue = parseFloat(getEntityFieldControlValue("fcb_rangehigh", true)); if (currentDefaultValue < currentMinimumValue) { alert("Oops! The Default quantity of " + currentDefaultValue + " is less than the Lowest Possible Quantity (" + currentMinimumValue + ") for this service! I am adjusting the default quantity to this Lowest Possible Quantity for you. Watch this:"); setEntityFieldValue("jc_defaultquantity", currentMinimumValue, true); } if (currentDefaultValue > currentMaximumValue) { alert("Oops! The Default quantity of " + currentDefaultValue + " is greater than the highest possible quantity (" + currentMaximumValue + ") for this service! I am adjusting the default quantity to this Highest Possible Quantity for you.Watch this:"); setEntityFieldValue("jc_defaultquantity", currentMaximumValue, true); } }

Now open the CRM form you want to edit, and register both of these web resources with the form:

Ok - now both scripts are on the page but dont do anything yet. The last step is to plug in the SetupEvents function into the CRM form's onload event.

On the same form, click the Add button for the Event handlers for the page, making sure that the control is Form and the event is OnLoad. Specify the Library as jc_ServiceScripts and the function name as setupEvents. Note - do not put parenthesis on the function name, otherwise the page will crash.


Run it. Now, when the form opens, the 3 fields fire the doValidateValues() function whenever the onkeyup event occurs on each respective field.

No comments:

Post a Comment