I know your big on both Model-Glue and cfajaxproxy, so I thought I would ask for your help using them together. If I have a Model-Glue app that has all of my objects defined and managed using ColdSpring, how would I call one of my service objects using the cfajaxproxy tag? All of the examples I have seen show calling the CFC's directly, but since my service object requires the injection of my DAO and Gateway objects, the direct call would fail. So I am a little stumped as to what to pass to the cfajaxproxy tag.
There are two answers to this (well I'm sure there are more, but two I'll focus on) problem. The simpler way, and the way I've done it in the past, is to not use the CFCs at all. Instead, my AJAX based actions will use normal Model-Glue events. So for example, I may have an event call getBlogEntriesJSON. This event handles broadcasting a message to get the blog entries, and then another method to handle converting the value to JSON. However, I've known for a while that ColdSpring had some support for automatically exposing itself to remote calls so I thought this would be a great opportunity to give it a try.
I began by creating a vanilla Model-Glue 2 application under webroot. I did this by editing the build.xml file within modelglueapplicationtemplate. This let me simply give the app a name, a base directory, and then right click to run it as an ANT task. I then verified that the application ran correctly. (Yes, even though I had just made a copy of the skeleton application and not touched a lick of code, I always make sure that the basic stuff works first before doing anything else!)
Next I decided on a simple model/service pair that I'd use for the demo. I created a testGateway in my model that had one method:
<cffunction name="getThings" access="public" returnType="query" output="false"> <cfset var q = queryNew("id,name")> <cfset var x = "">
<cfloop index="x" from="1" to="10"> <cfset queryAddRow(q)> <cfset querySetCell(q, "id", x)> <cfset querySetCell(q, "name", "Name #x#")> </cfloop>
<cfreturn q> </cffunction>
I then created a service component that would wrap this:
<cffunction name="init" access="public" returnType="any" output="false"> <cfargument name="gateway" type="any" required="true"> <cfset variables.gateway = arguments.gateway> </cffunction>
<cffunction name="getThings" access="public" returnType="query" output="false"> <cfreturn variables.gateway.getThings()> </cffunction>
And lastly, I defined them both within my ColdSpring.xml file:
<bean id="testGateway" class="testcoldspringremote.model.testgateway"> </bean>
<bean id="testService" class="testcoldspringremote.model.testservice"> <constructor-arg name="gateway"><ref bean="testGateway"/></constructor-arg> </bean>
This tells ColdSpring about my two components and also tells ColdSpring to create my service by passing in my gateway.
The last step in all of this was to then edit my controller and add a settestService method. Model-Glue supports auto wiring between ColdSpring and the controllers. By simply having setX in my controller, and X as a bean in ColdSpring.xml, I know that on startup, ModelGlue will pass an instance of X to setX. Here is the method in my controller:
<cffunction name="settestService" access="public" returnType="void" output="false"> <cfargument name="testService" type="any" required="true"> <cfset variables.testService = arguments.testService> </cffunction>
By the way, Model-Glue 3 makes this even easier. You just add beans="..." to your component tag. At this point, I wanted to test my setup. I edited my default home page event to broadcast a call to get things:
<event-handler name="page.index"> <broadcasts> <message name="getThings" /> </broadcasts> <results> <result do="view.template" /> </results> <views> <include name="body" template="dspIndex.cfm" /> </views> </event-handler>
And added the proper listener in my controller XML:
<controller name="MyController" type="testcoldspringremote.controller.Controller"> <message-listener message="OnRequestStart" function="OnRequestStart" /> <message-listener message="OnQueueComplete" function="OnQueueComplete" /> <message-listener message="OnRequestEnd" function="OnRequestEnd" /> <message-listener message="getThings" function="getThings" /> </controller>
Next I added the controller method:
<cffunction name="getThings" access="public" returnType="void" output="false"> <cfargument name="event" type="any"> <cfset arguments.event.setValue("things", variables.testService.getThings())> </cffunction>
The last thing was to edit the view:
<cfset things = viewState.getValue("things")> <cfdump var="#things#">
Wow, that was a lot of typing. Hopefully you get what I did. Basically, defined my beans in ColdSpring, wrote some dummy logic for them, then had Model-Glue actually make a call to them for my event so I could see, in the browser, the query. Just to prove I'm not crazy, here is the result:
Alright. My next stop was the ColdSpring docs. From that, it seemed like exposing my beans as remote proxies came down to two basic steps:
- Define (in XML) where the remote proxy will run and what it will represent
- Load the bean (ie, make an instance of it) so that a physical file is written out.
If we focus in on the relevant section of the docs (Using AOP to create remote proxies), our first step is to define the remote bean in XML. I won't repeat the docs. Here is what I used:
<bean id="testServiceRemote" class="coldspring.aop.framework.RemoteFactoryBean"> <property name="target"> <ref bean="testService" /> </property> <property name="serviceName"> <value>testService</value> </property> <property name="relativePath"> <value>/testcoldspringremote/remote/</value> </property> <property name="remoteMethodNames"> <value>get*</value> </property> </bean>
The only thing that tripped me up was relativePath. ColdSpring will not create the folder for you. I added a remote folder to my application root.
So the second step was to simply create an instance of the bean. So how would I do that? Well Model-Glue controllers can use an init method. You won't see them in the application template, but you can add your own to write application startup code. I added the following to my controller:
<cffunction name="init" access="public" returnType="any" output="false"> <cfargument name="ModelGlue" type="any" required="true" hint="I am an instance of ModelGlue."> <cfargument name="name" type="string" required="false" default="#createUUID()#" hint="A name for this controller."> <cfset var remoteBean = "">
<cfset super.init(arguments.modelglue,arguments.name)> <cfset remoteBean = getModelGlue().getBean("testServiceRemote")>
<cfreturn this> </cffunction>
There isn't anything too fancy in here really. The getBean method is defined in the Model-Glue docs. It's a pathway to ColdSpring and lets me grab (obviously) my defined beans. I reran my application again, went to the file system, and there it was - a file named testService.cfc.
Ok, so now for the fun part. I immediately tried to run this baby in my browser. I went to:
Sorry, a ColdSpring BeanFactory named was not found in application scope. Please make sure your bean factory is properly loaded. Perhapse your main application is not running?
This didn't make sense to me at first, but then it hit me. I wasn't running Model-Glue anymore! I was totally out of the MG context, and since MG had taken care of ColdSpring for me, it wasn't available to this CFC. Luckily this is easily solved. Joe describes the solution in this blog entry: Sharing MG ColdSpring Beans With Other Applications. You should read the blog entry. Basically, I just need to initialize and setup ColdSpring myself (not a big deal!) and then tell Model-Glue to use the same application variable. I
stoleborrowed the code from Joe's post exactly as he had it, with just the path to the XML file being a bit different.
After all of this, I of course went back to my normal web page, reloaded, and made sure Model-Glue was still cool. It was. (Whew!) I went back to my CFC, reloaded, and got the same error. And then I noticed it...
Sorry, a ColdSpring BeanFactory named was not
Do you see a missing word there? I looked in the error below and saw:
Sorry, a ColdSpring BeanFactory named #variables.beanFactoryName# was not found
So the code was looking for a beanFactoryName. I thought to myself - I bet that's the name of the Application scope variable that contains the factory. But since I didn't generate the service CFC, how do I set it? I couldn't find the solution in the docs, but a Google search found this blog entry by Bruce Philips. In this blog entry he mentioned a key named, appropriately enough, beanFactoryName. If you add this back in your ColdSpring.xml file, you would then end up with this definition:
<bean id="testServiceRemote" class="coldspring.aop.framework.RemoteFactoryBean"> <property name="target"> <ref bean="testService" /> </property> <property name="serviceName"> <value>testService</value> </property> <property name="relativePath"> <value>/testcoldspringremote/remote/</value> </property> <property name="remoteMethodNames"> <value>get*</value> </property> <property name="beanFactoryName"> <value>sharedBeanFactory</value> </property> </bean>
I reran everything again, went over to the CFC, and voila - I could hit my CFC!
Once I got past some of the kinks this worked like a charm, and once again I think we need to give props to both the ColdSpring and Model-Glue team.
I do have one final note on this. I'm using Model-Glue to init my bean and generate the physical file. I did that because, as I learned about this feature, that was the only way I had of handling "Do this on Application startup." Now that I ended up with actual code in the Application.cfm file, I'd probably move that bean-get call over to that file instead. It's possible that a) the proxy CFC may not exist and b) the first call may be to the remote proxy, not Model-Glue.