So I finally got an opportunity to use Model-Glue for a real application, and I showed my XML file to Joe... who suggested I blog my technique (which, it turns out, is also the technique he's been using). So now that the app is finished, I'm getting around to posting the blog entry in question.
I ended up with basically three tiers of events:
Take note of the fact that this enables the development of applications without using cflocation within the controller methods. It allows very well-structured applications, and maximizes code reuse. It also allows the application to make maximum use of the framework and forces the XML to be as self-documenting as possible. Aside from these factors, it creates a very artful structure within the application... there's no loose ends, every event runs from start to finish and all the bits fit together like puzzle pieces.
Personally I think this sort of structure is "true to the spirit of the framework" because it allows all the parts and pieces of the framework itself to do their jobs; using cflocation to call another event may work, but it allows the controller itself to handle flow control, which is a framework function. It also requires that the XML and the controller's code be reviewed to see what's happening within the application... and, probably worst of all, couples the controller to the events.
This is my controller section in a modelglue.xml file:
<controllers>
<controller name="myController" type="appName.controller.myController">
<message-listener message="OnRequestStart" function="OnRequestStart" />
<message-listener message="needLoginStatus" function="getLoginStatus" />
<message-listener message="needUserDetails" function="getUserDetails" />
<message-listener message="logUserAccess" function="writeEventToLogfile" />
<message-listener message="login" function="login" />
</controller>
</controllers>
It's in this section of the XML file that we map a the name by which a controller will be known to the framework to the type used by CFMX to instantiate it. We also create a message-to-method map for the controller's methods and the framework's events. The messages must have unique names and be mapped to the appropriate function name in the controller class outlined in the attributes of the controller tag.
<!-- This is enough XML to handle the one event showUserDetails -->
<!-- showUserDetails is a top-level event, on the URL -->
<event-handlers>
<event-handler name="showUserDetails">
<broadcasts>
<message name="needLoginStatus" />
</broadcasts>
<results>
<result name="isLoggedIn" do="userDetails" />
<result name="notLoggedIn" do="loginForm" />
</results>
</event-handler>
<!-- userDetails is a view-fetching event that issues a broadcast to get it's data -->
<event-handler name="userDetails">
<broadcasts>
<message name="needUserDetails" />
</broadcasts>
<views>
<include name="body" template="userDetails.cfm" />
</views>
<results>
<!-- note: layout is not dependent on a result by name therefore always runs -->
<result do="layout" />
</results>
</event-handler>
<!-- loginForm is a view-fetch event that exists for the notLoggedIn result of other events -->
<event-handler name="loginForm">
<views>
<include name="body" template="loginForm.cfm" />
</views>
<results>
<result do="layout" />
</results>
</event-handler>
<!-- layout includes layout.cfm with no name, hence it's genned to the screen by default -->
<event-handler name="layout">
<views>
<include name="layout" template="layout.cfm" />
</views>
</event-handler>
So you see, the URL "index.cfm?event=showUserDetail" just checks for login status and based on results triggers other events. Those other events trigger other events, and even those can trigger other events to accomplish the needs of the view and the system. If we wanted to add some functionality all we have to do is add other events...
<event-handler name="showUserDetails">
<broadcasts>
<message name="needLoginStatus" />
</broadcasts>
<results>
<result name="notLoggedIn" do="loginForm" />
<result name="isLoggedIn" do="userDetails" />
<result name="isLoggedIn" do="logAccess" />
</results>
</event-handler>
<event-handler name="logAccess">
<broadcasts>
<message name="logUserAccess" />
</broadcasts>
</event-handler>
Of course, in order to have this work, you'd have to add a message-handler to the controller block.
Alternatively, we can send an event-argument to the controller to modify it's behavior based on special circumstances:
<event-handler name="showUserDetails">
<broadcasts>
<message name="needLoginStatus">
<argument name="sendSessionUserToEvent" value="true" />
</message>
</broadcasts>
<results>
<result name="notLoggedIn" do="loginForm" />
<result name="isLoggedIn" do="userDetails" />
<result name="isLoggedIn" do="logAccess" />
</results>
</event-handler>
What's this do? Well, later on, when we look at actual controller code, we'll see that using an event argument and CFIF combine to make the controller method a lot more flexible than it may seem. Effectively, message/argument passes commands and arguments into the controller and results pass conditions from the controller back to the framework's kernel. In this case, the method that maps to the needLoginStatus will contain a cfif that checks for the sendSessionUserToEvent argument to be present and true, and if it's there and true it will connect session.user to arguments.event.
The controller, then, is responsible for only a very limited number of tasks, and switching events is NOT one of them.
From controller's perspective, the technique mentioned above can be illustrated with a simple code snippet:
<cfcomponent displayname="Controller" output="false" hint="I am a sample model-glue controller." extends="ModelGlue.Core.Controller">
<cffunction name="init" access="Public" returntype="cin" output="false" hint="I build a new SampleController">
<cfargument name="ModelGlue" required="true" type="ModelGlue.ModelGlue" />
<cfargument name="InstanceName" required="true" type="string" />
<cfset super.Init(arguments.ModelGlue) />
<cfreturn this />
</cffunction>
<!--- Functions specified by <message-listener> tags --->
<cffunction name="OnRequestStart" access="Public" returntype="void" output="false" hint="I am an event handler.">
<cfargument name="event" type="ModelGlue.Core.Event" required="true">
<cfset login(arguments.event)>
</cffunction>
<cffunction name="OnRequestEnd" access="Public" returntype="void" output="false" hint="I am an event handler.">
<cfargument name="event" type="ModelGlue.Core.Event" required="true">
</cffunction>
<cffunction name="OnQueueComplete" access="Public" returntype="void" output="false" hint="I am an event handler.">
<cfargument name="event" type="ModelGlue.Core.Event" required="true">
</cffunction>
<cffunction name="getUserDetails" access="Public" returntype="void" output="false" hint="I am an event handler.">
<cfargument name="event" type="ModelGlue.Core.Event" required="true">
<cfset arguments.event.setValue("userDetails","Jared,Rypka-Hauer,jared@web-relevant.com")>
</cffunction>
<cffunction name="getLoginStatus" access="Public" returntype="void" output="false" hint="I am an event handler.">
<cfargument name="event" type="ModelGlue.Core.Event" required="true">
<cfset var sFacade = "">
<cfif len(getAuthUser())>
<cfset arguments.event.addResult("isLoggedIn")>
<cfif arguments.event.argumentExists("sendSessionUserToEvent")>
<cfset sFacade = createObject("component","appName.model.facade").init(session)>
<!--- fix for cflogin/session timeout bug on CFMX 6.1 --->
<cfif not sFacade.exists("user")>
<!--- Assumes that cflogin sets email to username and
getUserInstance is a private controller method --->
<cfset sFacade.addValue("user",getUserInstance(getAuthUser())>
</cfif>
<cfset arguments.event.setValue("user",sFacade.getValue("user"))>
</cfif>
<cfelse>
<cfset arguments.event.addResult("notLoggedIn")>
</cfif>
</cffunction>
<cffunction name="login" access="Public" returntype="void" output="false" hint="I am an event handler.">
<cfargument name="event" type="ModelGlue.Core.Event" required="true">
<cflogin idletimeout="1200">
... yadda yadda ...
</cflogin>
</cffunction>
</cfcomponent>
So by comparing the XML to the controller code listed above, it's fairly obvious what's going on and how this all works. Really, using Model-Glue frees the developer to concentrate on the functionality required for the application and not on all the pieces that are required for navigation and control... because really if you're not using a framework, you're still using a framework, you just have to do what the framework does all for your own little self.
I hope this has helped someone. If so, feel free to drop me a note at jared@web-relevant.com to stroke my ego and get me to write more cool stuff.
Have a fab weekend...
Laterz!
J