Monday, October 20, 2014

OnChange part 1: Remove design-time event handlers

The Microsoft Dynamics CRM SDK comes with lots of functionality to affect how CRM behaves on the client side.
In a couple of articles I will discuss the opportunities ventilate my frustration over run-time event handling manipulation.

Events reacting to changes in fields can be defined design-time by making customizations to forms, and run-time by adding onChange event handlers by calling attribute.addOnChange(eventHandler).
To change the behavior run-time, there is also a method to remove designated event handlers by calling attribute.removeOnChange(eventHandler).
Read more about this in the SDK.

It is however not possible to remove event handlers that were added design-time.

The definition of methods addOnChange and removeOnChange are quite straight forward. You pass a function pointer to the method you want to add or remove. Much like you do when you define event handlers design-time.

account.name.onchange

In a scenario where a field is designed to have a specific event handler and you want to remove it to be replaced by another one, you would think that you call removeOnChange with the method name as entered in the customizations.

Sample form library:

Cinteros.JR.account = {
    formLoad: function () {
        var nameAttribute = Xrm.Page.getAttribute("name");
        nameAttribute.removeOnChange(Cinteros.JR.account.name_onChange);
        nameAttribute.addOnChange(Cinteros.JR.account.name_onChange_special);
    },

    name_onChange: function () {
        // Do standard things with name attribute
        Xrm.Utility.alertDialog("Normal function"); 
    },

    name_onChange_special: function () {
        // Do special things with name attribute
        Xrm.Utility.alertDialog("Special function"); 
    }
}

I discovered that the call to removeOnChange had no effect in this scenario. But the addOnChange added the new method as event handler. So I started digging using the F12 debugger in my browser. What does the function really do? And why does it fail silently?

All events firing on field changes in a form are stored in one of two arrays. The first one contains “Sys.EventHandlerList” items that are events defined by CRM itself or by customization. The second one contains the “dynamic events” added by the addOnChange method.
When removing events, both of these arrays are investigated for the function to remove. So why then does it not find and remove the function as I want it to?

The answer lies in what is actually contained in these arrays. When I step my way down the internal functions of CRM, I am able to investigate what an event handler from customization according to the image above will result in within that array, and how it is matched from the removeOnChange function.

The list of event handlers from customization above contains this:

[
    function name_onchange_handler(eventObj, eventArgs) {
        try {
            var eContext = Mscrm.FormUtility.constructExecutionObject(eventObj, 0, null, null);
            eContext = Mscrm.FormUtility.constructExecutionObject(eventObj, 0, null, eContext);
            var t_s = new Date().getTime();
            Cinteros.JR.account.name_onChange();
            var t_e = new Date().getTime();
            Mscrm.MetricsReporting.instance().addMetric('form_event_onchange_function_Cinteros.JR.account.name_onChange', t_e - t_s);
        } catch (e) {
            displayError('name', 'onchange', e.description);
        }
    }
]

As you can see the function name that was entered in the customizations is now wrapped to extract the context (which is not even used after extraction in this case) and some timing and metrics, and of course the try-catch-wrapper.

And the function reference passed to removeOnChange contains – not the name of the function in my javascript, but the contents, which equates to this:

function () { 
    // Do standard things with name attribute 
    Xrm.Utility.alertDialog("Normal function"); 
}

There is no hint at all to what the actual method to remove was, simply the code that would be executed.

So when the remove function calls Array.remove(list, item), there will of course be no match as the code block above is not found in the array and the customized event handler will still fire for every onChange event on the field.

The only way to actually remove a design-time event handler, I guess, would be to perform the same wrapping when you call the removeOnChange, something like this:

nameAttribute.removeOnChange(function name_onchange_handler(eventObj,eventArgs) { try { var eContext=Mscrm.FormUtility.constructExecutionObject(eventObj,0,null,null); eContext=Mscrm.FormUtility.constructExecutionObject(eventObj,0,null,eContext); var t_s = new Date().getTime(); Cinteros.JR.account.name_onChange(); var t_e = new Date().getTime(); Mscrm.MetricsReporting.instance().addMetric('form_event_onchange_function_Cinteros.JR.account.name_onChange', t_e - t_s); } catch(e) { displayError('name', 'onchange', e.description); } });

Unfortunately, I haven’t been able to come up with the correct formatting of this string to make it work. And it would probably not be very supported.

(I sincerely hope someone will give me a sarcastic comment on my grave misunderstanding, and a pointer to how this should be done, and I would have to post an official apology to the Dynamics team… please, please, correct and enlighten me! :)

---

To Be Continued… OnChange part 2: addOnChange with context awareness

No comments:

Post a Comment