views:

1439

answers:

3

I'm rusty with delegates and closures in JavaScript, and think I came across a situation where I'd like to try to use one or both.

I have a web app that behaves a lot like a forms app, with fields hitting a server to change data on every onBlur or onChange (depending on the form element). I use ASP.NET 3.5's Web Services and jQuery to do most of the work.

What you need to know for the example:

  • isBlocking() is a simple mechanism to form some functions to be synchronous (like a mutex)
  • isDirty(el) checks to make sure the value of the element actually changed before wasting a call to the server
  • Agent() returns a singleton instance of the WebService proxy class
  • getApplicationState() passes a base-64 encoded string to the web service. This string represents the state of the application -- the value of the element and the state are passed to a service that does some calculations. The onSuccess function of the web service call returns the new state, which the client processes and updates the entire screen.
  • waitForCallback() sets a flag that isBlocking() checks for the mutex

Here's an example of one of about 50 very similar functions:

function Field1_Changed(el) {
    if (isBlocking()) return false;
    if (isDirty(el)) {
        Agent().Field1_Changed($j(el).val(), getApplicationState());
        waitForCallback();
    }
}

The big problem is that the Agent().Field_X_Changed methods can accept a different number of parameters, but it's usually just the value and the state. So, writing these functions gets repetitive. I have done this so far to try out using delegates:

function Field_Changed(el, updateFunction, checkForDirty) {
    if (isBlocking()) return false;
    var isDirty = true; // assume true
    if (checkForDirty === true) {
        isDirty = IsDirty(el);
    }
    if (isDirty) {
        updateFunction(el);
        waitForCallback();
    }
}

function Field1_Changed(el) {
    Field_Changed(el, function(el) { 
        Agent().Field1_Changed($j(el).val(), getTransactionState()); 
    }, true);
}

This is ok, but sometimes I could have many parameters:

    ...
    Agent().Field2_Changed($j(el).val(), index, count, getApplicationState());
    ....

What I'd ultimately like to do is make one-linen calls, something like this (notice no getTransactionState() calls -- I would like that automated somehow):

// Typical case: 1 value parameter
function Field1_Changed(el) {
    Field_Changed(el, delegate(Agent().Field1_Changed, $j(el).val()), true);
}

// Rare case: multiple value parameters
function Field2_Changed(el, index, count) {
    Field_Changed(el, delegate(Agent().Field1_Changed, $j(el).val(), index, count), true);
}

function Field_Changed(el, theDelegate, checkIsDirty) {
    ???
}

function delegate(method) {
    /* create the change delegate */
    ???
}

Ok, my first question is: Is this all worth it? Is this harder to read but easier to maintain or the other way around? This is a pretty good undertaking, so I may end up putting a bounty on this one, but I'd appreciate any help you could offer. Thanks!

UPDATE

So, I've accepted an answer based on the fact that it pointed me in the right direction. I thought I'd come back and post my solution so that others who may just be starting out with delegates have something to model from. I'm also posting it to see if anybody wants to try an optimize it or make suggestions. Here's the common Field_Changed() method I came up with, with checkForDirty and omitState being optional parameters:

function Field_Changed(el, args, delegate, checkForDirty, omitState) {
    if (isBlocking()) return false;
    if (!$j.isArray(args) || args.length == 0) {
        alert('The "args" parameter in Field_Changed() must be an array.');
        return false;
    }
    checkForDirty = checkForDirty || true; // assume true if not passed
    var isDirty = true; // assume true for updates that don't require this check
    if (checkForDirty === true) {
        isDirty = fieldIsDirty(el);
    }
    if (isDirty) {
        omitState = omitState || false; // assume false if not passed
        if (!omitState) {
            var state = getTransactionState();
            args.push(state);
        }
        delegate.apply(this, args);
        waitForCallback();
    }
}

It handles everything I need it to (check for dirty, applying the application state when I need it to, and forcing synchronous webservice calls. I use it like this:

function TransactionAmount_Changed(el) {
    Field_Changed(el, [cleanDigits($j(el).val())], Agent().TransactionAmount_Changed, true);
}

cleanDigits strips out junk characters the user may have tried to type in. So, thanks to everyone, and happy coding!

+3  A: 
Chetan Sastry
Nice, I never thought of passing all the arguments to the Field_Changed function. I'll give it a try (may make a few minor changes). I think this will point me in the right direction.
Cory Larson
Hey, what's the purpose of the "this" in delegate.apply(this, args); ?
Cory Larson
It's the context object for the function being called i.e the value of 'this' inside the function. You can use whatever you want there.
Chetan Sastry
A: 

I have been using the following utility function that I wrote a long time ago:

/**
 * @classDescription This class contains different utility functions
 */
function Utils()
{}

/**
 * This method returns a delegate function closure that will call
 * targetMethod on targetObject with specified arguments and with
 * arguments specified by the caller of this delegate
 * 
 * @param {Object} targetObj - the object to call the method on
 * @param {Object} targetMethod - the method to call on the object
 * @param {Object} [arg1] - optional argument 1
 * @param {Object} [arg2] - optional argument 2
 * @param {Object} [arg3] - optional argument 3
 */
Utils.createDelegate = function( targetObj, targetMethod, arg1, arg2, arg3 )
{
 // Create an array containing the arguments
 var initArgs = new Array();

 // Skip the first two arguments as they are the target object and method
 for( var i = 2; i < arguments.length; ++i )
 {
  initArgs.push( arguments[i] );
 }

 // Return the closure
 return function()
 {
  // Add the initial arguments of the delegate
  var args = initArgs.slice(0);

  // Add the actual arguments specified by the call to this list
  for( var i = 0; i < arguments.length; ++i )
  {
   args.push( arguments[i] );
  }

  return targetMethod.apply( targetObj, args );
 };
}

So, in your example, I would replace

function Field1_Changed(el) {
    Field_Changed(el, delegate(Agent().Field1_Changed, $j(el).val()), true);
}

With something along the lines

function Field1_Changed(el) {
    Field_Changed(el, Utils.createDelegate(Agent(), Agent().Field1_Changed, $j(el).val()), true);
}

Then, inside of Agent().FieldX_Changed I would manually call getApplicationState() (and encapsulate that logic into a generic method to process field changes that all of the Agent().FieldX_Changed methods would internally call).

niktech
This looked good, up until your suggestion to call getApplicationState() inside the FieldX_Changed methods. The methods on the Agent are generated automatically by .NET and I have no control over them. Guess I should have mentioned that in the question.
Cory Larson
A: 

Closures and delegates in JavaScript: http://www.terrainformatica.com/2006/08/delegates-in-javascript/ http://www.terrainformatica.com/2006/08/delegates-in-javascript-now-with-parameters/

c-smile