The ContactManager is a fairly simple example application that shows some of the power of Mach II. We'll take it apart, piece by piece and explain how it all works.
Structure
First of all, we'll look at the overall (directory) structure of the application:
ContactManagerApplication.cfm- This just ensures session management is enabled.
index.cfm- The default entry point, it simply
cfinclude'smach-ii.cfmbelow.
- The default entry point, it simply
mach-ii.cfm- Mach II's entry point. This initializes the framework as necessary
per the
MACHII_CONFIG_MODEsetting (see the Getting Started section for more information on that).
- Mach II's entry point. This initializes the framework as necessary
per the
configmach-ii.xml- The application configuration file that defines the controller behavior, events, event handlers, filters, views and so on. We'll go into this in much more detail below.
filters- This directory contains CFCs that implement event filters. The example
filter ensures that the event object contains a
Contactobject, optionally constructing it from lower-level data in the event object (i.e., from form data).
- This directory contains CFCs that implement event filters. The example
filter ensures that the event object contains a
model- This directory contains CFCs that implement the business logic behind the
application. The example contains CFCs that model a single
Contact, theContactManageritself and the notion ofRecentContacts.
- This directory contains CFCs that implement the business logic behind the
application. The example contains CFCs that model a single
views- This directory contains
.cfmpages that implement views (or partial views). The example contains pages that render lists of contacts, contact entry forms etc as well as a template for all the views,mainTemplate.cfm.
- This directory contains
mach-ii.xml
The Mach II website has some documentation on Configuring Mach II that explains some of the XML configuration file. The Mach II DTD also contains information about what the XML configuration file means. Next we'll take a look at the configuration file for the ContactManager sample application, line-by-line:
<mach-ii version="1.0">
<!-- PROPERTIES -->
<properties>
<property name="defaultEvent" value="showHome" />
<property name="exceptionEvent" value="exception" />
<property name="applicationRoot" value="/ContactManager" />
<property name="eventParameter" value="event" />
<property name="parameterPrecedence" value="form" />
<property name="maxEvents" value="10" />
</properties>
In the properties section, we define six values:
defaultEvent- this is the name of the event that is used when no explicit event is given on the request URL.exceptionEvent- this is the name of the event that is generated if an exception occurs within the application and is not caught by the application, i.e., the exception reaches the Mach II framework.applicationRoot- this is the root-relative URL of the application itself, used to construct paths to views and so on.eventParameter- this is the name of the URL parameter that defines the event being passed in (likefuseaction=for Fusebox applications).parameterPrecedence- this determines whetherformorURLscope takes precedence, i.e., ifform.fooandURL.fooare both present, this property determines which one gets used (form.fooin this case).maxEvents- the maximum number of events to process for a single request: since event handlers can generate new events, this helps prevent runaway requests generating too many events!
<!-- LISTENERS -->
<listeners>
<listener name="ContactManager" type="ContactManager.model.ContactManager">
<invoker type="MachII.framework.invokers.CFCInvoker_EventArgs" />
</listener>
<listener name="RecentContacts" type="ContactManager.model.RecentContacts">
<invoker type="MachII.framework.invokers.CFCInvoker_EventArgs" />
<parameters>
<parameter name="maxRecentContacts" value="3" />
</parameters>
</listener>
</listeners>
This defines two listeners. A listener is a CFC that extends MachII.framework.Listener and
which is part of the "model" for the application. Each listener defines how
it is invoked - either with an event object (MachII.framework.invokers.CFCInvoker_Event)
or with event arguments (MachII.framework.invokers.CFCInvoker_EventArgs).
The former passes an event argument to methods on the listener,
the latter passes each of the event object's arguments as separate arguments
to methods on the listener.
Which you use will be a matter of style. The ContactManager listeners expect
to get a variety of arguments passed, depending on what data was available
to the event itself (form and / or URL data, mostly).
<!-- EVENT FILTERS -->
<event-filters>
<event-filter name="eventArgs" type="MachII.filters.EventArgsFilter" />
<event-filter name="requiredFields" type="MachII.filters.RequiredFieldsFilter" />
<event-filter name="contactBeaner" type="ContactManager.filters.ContactBeanerFilter" />
</event-filters>
This defines three event filters. Event filters are CFCs that extend MachII.framework.EventFilter and define a filterEvent method. Event filters are passed the current event,
the current event context and a sequence of name / value parameters. The specific
event filters defined here perform the following tasks:
eventArgs- this uses the filter parameters to set arguments in the current event (and is, essentially, a synonym for theevent-argevent handler operation (see below).requiredFields- this takes a list of fields (requiredFields) and an event name (invalidEvent) and tests that all the specified fields are present in the current event as arguments (i.e., they were passed asformorURLdata). If any field is missing, the specified event is generated with two initial arguments:message(a plain English error message) andmissingFields(a comma-separated list of the fields that were not present).contactBeaner- this is part of the ContactManager application and is used to assemble theform/URLdata fields into a singleContactobject within the event object. This is very similar to how Struts deals with form submissions.
<!-- EVENT-HANDLERS -->
<event-handlers>
<event-handler event="showHome" access="public">
<notify listener="RecentContacts" method="getRecentContacts" resultKey="request.recentContacts" />
<view-page name="mainMenu" contentKey="request.content" />
<view-page name="mainTemplate" />
</event-handler>
The event handler for the showHome event - the default event.
It is a public event handler so it can handle events generated by users (in
links and form actions). This calls getRecentContacts() on
the CFC behind the RecentContacts listener
(which is ContactManager.model.RecentContacts) and stores the
result in request.recentContacts (the resultKey).
Then it renders the mainMenu view, which displays request.recentContacts,
and stores the output in request.content (the contentKey).
Finally it renders the
mainTemplate view,
which wraps request.content in a layout (and outputs the HTML
because no resultKey is specfied).
<event-handler event="newContact" access="public">
<!-- One way to set event-args... -->
<event-arg name="submitEvent" value="createContact" />
<event-arg name="submitLabel" value="Create" />
<announce event="showContactForm" copyEventArgs="true" />
</event-handler>
The event handler for the newContact event - the "Add Contact" link. This
uses the event-arg operation to set two arguments within the event and generates
a new event (showContactForm) with the current event's argument copied in -
including the two arguments set in this handler. See showContactForm below
for how processing continues at this point.
<event-handler event="editContact" access="public">
<notify listener="ContactManager" method="getContact" resultKey="request.contact" />
<!-- Second way to set event-args... -->
<filter name="eventArgs">
<parameter name="submitEvent" value="updateContact" />
<parameter name="submitLabel" value="Update" />
</filter>
<announce event="showContactForm" copyEventArgs="true" />
</event-handler>
The event handler for the editContact event - the "edit" link
when viewing a contact. The link passes id= which is copied into
the event object. This handler calls the getContact() method on
the CFC behind the ContactManager listener (which is ContactManager.model.ContactManager),
passing in the id value, and stores the result - the retrieved contact - in
request.contact (the
resultKey). Then it uses
the eventArgs filter
to set two arguments within the event object - an alternative to how newContact above
is processed. Again, the showContactForm event is generated to
continue processing.
<event-handler event="showContactForm" access="public">
<filter name="contactBeaner" />
<view-page name="contactForm" contentKey="request.content" />
<view-page name="mainTemplate" />
</event-handler>
The event handler for the showContactForm event - used by both
add and edit above. This uses the contactBeaner filter to ensure
that a properly constructed
Contact object is present in the event object - taken from request.contact in
the case of edit or from the event in the case of add (which, clearly, is initially
empty). The contactForm view is rendered into request.content which
is then presented as HTML by the mainTemplate view. Note that
the contents of the submit button and the event passed to the form action are
both determined by event arguments set in the event handlers above (submitLabel and submitEvent respectively).
<event-handler event="createContact" access="public">
<filter name="requiredFields">
<parameter name="requiredFields" value="firstName,lastName,street,city,state,zip" />
<parameter name="invalidEvent" value="newContact" />
</filter>
<notify listener="ContactManager" method="createContact" resultKey="request.createdContact" />
</event-handler>
The event handler for the createContact event - the submitted
event for adding a contact. This uses the requiredFields filter
to check that certain fields are present (and, if not, generates the newContact event
and aborts this handler by returning false - processing continues with the newContact event
above but now the event object contains the message and missingFields information
as well as the contact information already entered). If all the fields are
present,
the createContact() method on the ContactManager listener
is called and the result stored in request.createdContact. The
createContact() method generates a contactCreated event to continue processing
- see below.
<event-handler event="updateContact" access="public">
<filter name="requiredFields">
<parameter name="requiredFields" value="firstName,lastName,street,city,state,zip" />
<parameter name="invalidEvent" value="newContact" />
</filter>
<notify listener="ContactManager" method="updateContact" resultKey="request.createdContact" />
</event-handler>
Very similar to the event handler for createContact above, this
handler for
updateContact also checks required fields but calls updateContact() on
the listener (instead of createContact()) which generated a contactUpdated event to continue processing. Otherwise the behavior is as above.
<event-handler event="contactCreated" access="private">
<notify listener="RecentContacts" method="addRecentContact" />
<announce event="listContacts" /> </event-handler>
The handler for contactCreated - generated by the createContact() call
above - simply calls addRecentContact() and then generates
a listContacts event (to cause the contacts to be redisplayed).
It is a private event handler so it can only be invoked for
events generated by the application itself, not by events generated
by the user.
<event-handler event="contactUpdated" access="private">
<notify listener="RecentContacts" method="addRecentContact" />
<announce event="listContacts" />
</event-handler>
Very similar to the handler for contactCreated above, this handles the
contactUpdated event generated by the updateContact() call
above.
Generating different events for createContact() and updateContact() allows
more flexibility for future enhancements, even though in this
simple application it seems to add complexity and duplication.
An alternative way to handle these two events - since they
currently do the same thing - would be to have both of their
handlers simply announce a new event, say, contactTouched,
and add a handler for that which adds the contact to the "recent"
list and then announces listContacts to redisplay the contacts.
If there were additional events in the system that were all
handled this same way, this might be a good way to reduce the
duplicate calls to addRecentContact().
<event-handler event="listContacts" access="public">
<notify listener="RecentContacts" method="getRecentContacts" resultKey="request.recentContacts" />
<notify listener="ContactManager" method="getAllContacts" resultKey="request.allContacts" />
<view-page name="contactList" contentKey="request.content" />
<view-page name="mainTemplate" />
</event-handler>
The handler for listContacts calls getRecentContacts() on the RecentContacts listener and then calls getAllContacts() on the ContactManager listener, storing both results in request scope variables for
use by the contactList view which it renders into request.content and then finally renders the mainTemplate view as HTML.
<event-handler event="viewContact" access="public">
<notify listener="ContactManager" method="getContact" resultKey="request.contact" />
<view-poge name="viewContact" contentKey="request.content" />
<view-page name="mainTemplate" />
</event-handler>
The handler for viewContact - the "View Contact" link in the application
- which is passed an id (in the URL - it becomes an event argument).
The handler retrieves the specified contact, just like the
editContact handler did, above, and then renders the viewContact view into a variable and the mainTemplate view to HTML.
<event-handler event="cancelUpdateContact" access="public">
<announce event="listContacts" />
</event-handler>
<event-handler event="cancelCreateContact" access="public">
<announce event="listContacts" />
</event-handler>
These two handlers are for events generated by pressing "Cancel" on the
update and add/create contact forms respectively. They both
simply generate the listContacts event to redisplay the main
list of contacts.
<event-handler event="exception" access="private">
<view-page name="exception" contentKey="request.content" />
<view-page name="mainTemplate" />
</event-handler>
</event-handlers>
The final event handler is for the exception event - which is generated if
an exception occurs but is not handled by your code and propagates out to the
Mach II framework. See the exceptionEvent property at the beginning of the
XML file.
<!-- PAGE-VIEWS -->
<page-views>
<page-view name="mainTemplate" page="/views/mainTemplate.cfm" />
<page-view name="mainMenu" page="/views/mainMenu.cfm" />
<page-view name="contactList" page="/views/listContacts.cfm" />
<page-view name="viewContact" page="/views/viewContact.cfm" />
<page-view name="contactForm" page="/views/contactForm.cfm" />
<page-view name="exception" page="/views/exception.cfm" />
</page-views>
This section defines the various HTML views used by the application. The page attribute
paths are relative to the applicationRoot defined in the properties
section above.
<!-- PLUGINS -->
<plugins>
</plugins>
</mach-ii>
There are no plugins in the sample application. I'll write more about plugins later.
- The Mach-II website.
- The Mach-II for CFML mailing list (on Google Groups).
- Resources on this website: