Creating a Simple Event Handler in ColdFusion Using Dynamic Method Injection

Posted on Aug 06, 2010

That’s quite a title, isn’t it? In case it leaves some of you scratching your heads, wondering what I might be talking about, let me explain the concept. Recently, I’d been using Mura CMS a lot and one of the nice things it has (like other projects such as Mango or frameworks in general) is the idea of events. Essentially, the application announces an event (for example “onSiteLogin” is a built in one in Mura, but you can create your own) and, assuming you have defined a function in a hanlder to handle that event, this function will be called. This allows you to organize and reuse your code easily because these events can be announced anywhere in your application but you only have one location where they are handled. I set about creating a basic event handler proof of concept myself, and this is what I ended up with.

Personally, I like the concept of event-based programming since its similar to Flex/Flash, though in ColdFusion there really isn’t a concept similar to the viewstack so the idea of bubbling doesn’t seem to make as much sense (at this point, its worth noting that frameworks such as Sean Corfield’s Edmund already ported the idea of event-based programming to CF however this code is not based upon that framework). In this case, a friend of mine had created a framework that would announce events in JavaScript that would be passed to a CFM handler. The CFM handler parsed the event name and various values from the URL and contained a bunch of case statements defining how these events were handled. I thought there might be a better way, so I started on this journey of creating an eventHandler component that you could know how to do a couple of key things:

  • register an event listener - this allows me to separate out my processing code for an event rather than have everything in a monolithic CFM/CFC.
  • announce an event - the event handler should know how to invoke the proper method on the proper handler based upon my event type. I did this using a convention whereby and event of type “InsertComment” would look for a method called “handleInsertComment.”

I don’t go into how you would create a CFM or CFC means of invoking these events that could be called form JavaScript but its really not that difficult. One means would simply be to create a eventHandlerRemote.cfc that wraps the announceEvent call and can be imported into any page with <cfajaxproxy>.

Injecting Methods into My Event Handler
In looking into Mura’s code, it seemed to handle calling methods in your eventHandlers by keeping an array of handlers and looping through them (this may have changed and I am definitely oversimplifying the description). I thought that perhaps there might be an easier way. In my event handler, when you register a new event listener, it actually goes through the listener CFC and sucks all the public methods into itself (ColdFusion makes injecting methods into a CFC on the fly very easy). For example, if you register an event listener named commentListener.cfc within my base eventHandler.cfc, every method in commentListener.cfc will now become a method of eventHandler.cfc. This makes it very easy for me to call the methods when an event is announced without needing to keep an array of handlers and/or methods and search through them. I simply wrap the call in a try catch and try running the method.

I should note that I have no idea if there are performance or other concerns with this technique when loading a ton of listeners. Anyway, with that aside, let’s look at the code for the eventHandler.cfc.

component output="false"
{
   public eventHandler function init() {
      return this;
   }
   
   public void function registerEventListener(required string path) {
      // if this is a directory, just loop through and load all the CFCs
      if (directoryExists(arguments.path)) {
         // recursively get all cfc's in the path
         local.components = directoryList(arguments.path,true,"path","*.cfc");
         for (var cfcPath in local.components) {
            registerCFCfromPath(cfcPath);
         }
      }
      // if its just a file path
      else if (fileExists(arguments.path)) {
         registerCFCfromPath(arguments.path);
      }
      // assume its just a dot notation component
      else {
         registerCFC(path);
      }
   }
   
   public void function announceEvent(required event e) {
      local.functionName = "handle" & e.getEventType;
      try {
         local.methodName = "handle" & e.getEventType();
         // yes, that's right, I am using evaluate. sue me.
         evaluate("this.#local.methodName#(e)");
      }
      catch (any exception) {
         // here I do some brilliant exception handling
      }
   }
   
   private void function registerCFCfromPath(required string cfcPath) {
      local.cfcDotNotation = replaceNoCase(replace(getRelative(cfcPath),"/",".","all"),".cfc","");
      registerCFC(local.cfcDotNotation);
   }
   
   private void function registerCFC(required string cfcDotPath) {
      local.cfc = createObject(arguments.cfcDotPath);
      local.cfcMeta = getMetaData(local.cfc).functions;
      // doing a for in loop broke here for some reason
      for (var i=1;i <= arrayLen(local.cfcMeta);i++) {
         if (not structKeyExists(local.cfcMeta[i],"access") || local.cfcMeta[i].access == "public" || local.cfcMeta[i].access == "remote") {
            this[local.cfcMeta[i].name] = local.cfc[local.cfcMeta[i].name];
         }
      }
   }
   
   /**
   * Returns a relative path from the current template to an absolute file path.
   *
   * @param abspath Absolute path. (Required)
   * @return Returns a string.
   * @author Isaac Dealey (info@turnkey.to)
   * @version 1, May 2, 2003
   */
   private string function getRelative(abspath) {
    var aHere = listtoarray(getdirectoryfrompath(getcurrenttemplatepath()),"\/");
    var aThere = ""; var lenThere = 0;
    var aRel = ArrayNew(1); var x = 0;
    var newpath = "";
   
    aThere = ListToArray(abspath,"\/"); lenThere = arraylen(aThere);
   
    for (x = 1; x lte arraylen(aHere); x = x + 1) {
    if (x GT lenThere OR comparenocase(aHere[x],aThere[x])) {
    ArrayPrepend(aRel,".."); if (x lte lenThere) { ArrayAppend(aRel,aThere[x]); }
    }
    }
   
    for (; x lte arraylen(aThere); x = x + 1) { ArrayAppend(aRel,aThere[x]); }
   
    newpath = ArrayToList(aRel,"/");
   
    return newpath;
   }
}

I tried to make the registerEventListener() method very flexible. As you can see above, it takes a path which can be a dot notation to component (i.e. “com.email.sendEmailListener”) or a file path to that component (i.e. expandPath(“com/email/sendEmailListener.cfc”)) or a directory path (i.e. expandPath(“com”)). The directory path is the most useful in my opinion since, assuming all your listeners are in the same directory it will load all of them (and will recurse through any subdirectories).

Once it finds all the CFCs it instantiates them and uses the getMetaData() method on them to pull a list of functions (look at the registerCFC() method). I loop through them and if the method is public or remote, I add it to the this scope of my eventHandler.cfc instance, thereby making it a local method.

The announceEvent() method simply creates a method name using the event type and tries to invoke it on this component. Assuming it exists and is set up to receive the event, it will run.

The Event Object
For what its worth, its assumed that each of these listener functions will take an instance of an event for which I have created a simple event.cfc. The event component takes an eventType, like you’d find in Flex for instance, and also copies of the form and url scopes, like most frameworks do. As you can see in the code below, I essentially hardcode the form scope to take precedence when setting values. Also, I added some methods that you will need when receiving an event for seeing if a value exists on the event, setting a new value, getting all the values or getting a specific value (it’ll return empty string if the value doesn’t exist).

component output="false" accessors="true"
{
   property name="eventType" type="string" required="true";

   public function init(
      string eventType="",
      struct formStruct={},
      struct urlStruct={}
   ) {
      variables.values = {};
      setValues(arguments.urlStruct);
      setValues(arguments.formStruct);
      setEventType(arguments.eventType);
   }
   
   public function getValue(required string valueName) {
      local.return = "";
      
      if (hasValue(arguments.valueName)) {
         local.return = variables.values[arguments.valueName];
      }
      return local.return;
   }
   
   public function getValues() {
      return variables.values;
   }
   
   public function hasValue(required string valueName) {
      return structKeyExists(variables.values,arguments.valueName);
   }
   
   public function setValue(required string key,required any value) {
      variables.values[arguments.key] = arguments.value;   
   }
   
   private function setValues(required struct values) {
      structAppend(variables.values,arguments.values);   
   }
}

Registering Listeners & Creating the Event
In order to use this event handler, we will need to create it in application scope and then we can register handlers within it. Below, for example, is my onApplicationStart() method of Application.cfc which creates the eventHandler instance and registers all my listeners under the listeners folder:

<cffunction name="onApplicationStart" returnType="boolean" output="false">
   <cfset application.eventHandler = new eventHandler() />
   <!--- register any event handlers we may want --->
   <cfset application.eventHandler.registerEventListener(expandPath("listeners")) />
   
   <cfreturn true />
</cffunction>

I also tweaked my onRequestStart method to create the event for each request and also to make it easy to call the announceEvent() method on the eventHandler. The event is initialized with everything in the form and url scope. I also add a method on the request scope to call the eventHandler’s announceEvent method (its just a copy of another method in Application.cfc which simply calls the application scoped handler).

<cffunction name="onRequestStart" returnType="boolean" output="false">
   <cfargument name="thePage" type="string" required="true" />
   
   <cfif structKeyExists(url,"reload")>
      <cfset onApplicationStart() />
      <cfset onSessionStart() />
   </cfif>
   
   <cfset request.event = new Event("",form,url) />
   
   <cfset request.announceEvent = this.announceEvent />
      
   <cfreturn true />
</cffunction>

<cffunction name="announceEvent" access="public" output="false" returntype="void">
   <cfargument name="event" required="true" type="event" />
   
   <cfset application.eventHandler.announceEvent(event) />
</cffunction>

How Do I Use It?
Now that we’ve covered how this eventHandler works and is set up, let’s actually show how you might use it. Below is a simple event listener which might be used for sending emails on a “sendEmail” event. Right now, all it does is add a value to the event so that we can later see that it did run as expected, but you could use the form, URL or other values in the event to make a generic method that would send an email when this event is announced (perhaps the to, from and content are contained in the event values).

component output="false"
{
   public event function handleSendEmail(required event event) {
      // here we might put code to send an email
      
      event.setValue("emailSent",true);
      return event;
   }
}

To announce this in your code, you’d just do as in the below code. Basically, I modify the type in the request.event and announce the event. Since the same event is peristed across the event, I can then even modify the eventType and re-announce it so that it will run a new listener method but will still have any values set by the prior listener method.

<cfset request.event.setEventType("InsertComment") />
<cfset request.announceEvent(request.event) />
<cfdump var="#request.event#" />
<cfdump var="#request.event.getValues()#" />

<cfset request.event.setEventType("SendEmail") />
<cfset request.announceEvent(request.event) />
<cfdump var="#request.event.getValues()#" />

That code, will output the below on screen:

output on screen

As you can see, the first time the “InsertComment” event is announced and that method added a value called “commentInserted” to the event (just for example purposes, we could add whatever we want to the event) and then when the “SendEmail” event is announced, it also appended a value for “emailSent.”

Going Forward
As I mentioned earlier, this is just a simple proof of concept. It is similar to something that I created with the specific intent of handling events passed from a simple JavaScript “framework” and it could be used for that or as a very simple “framework” of sorts in your ColdFusion code. Honestly, I just thought it was a fun experiment.

Comments

John Allen That is pretty slick.

Posted By John Allen / Posted on 08/10/2010 at 10:51 AM


Rainer My idea is that this is not just a fun experiment, but it also has much potentional!

At the moment, I am developing an app, based on Railo BER 3.2 (including Hibernate implementation) and ColdSpring Injection framework.

I am looking for a serious Event handling framework, without having to start using a framework like ColdBox for that. I think ColdBox is great, but I also think I introduce much overhead with that framework 'cause I already use Hibernate and Coldspring.

I will start using this great Event Handler sample above, and hope this will be growing into a serious framework!

Thanks guys!

Cheers Rainer.

Posted By Rainer / Posted on 10/02/2010 at 11:52 AM


Write your comment



(it will not be displayed)





About

My name is Brian Rinaldi and I am the Web Community Manager for Flash Platform at Adobe. I am a regular blogger, speaker and author. I also founded RIA Unleashed conference in Boston. The views expressed on this site are my own & not those of my employer.