Development Guide - Mach-II/Mach-II-Framework GitHub Wiki
THIS DOCUMENT IS OUTDATED AND INCOMPLETE!
We are in the process of updating it for Mach-II 1.8. It was originally done by Sean Corfield for Macromedia many moons ago, and it was updated in July of 2007 for Mach-II 1.1.1. Obviously Mach-II has changed a lot since then so much in this guide will be dated, but it's important enough as a resource to get it out there.
The purpose of this document is to provide general guidelines for developing CFML applications using the Mach-II framework. Mach-II is an object-oriented, Model-View-Controller (MVC) framework that implements an Implicit Invocation architecture, and is designed to help CFML developers build applications that are more easily maintained over the life of the application.
This document is an updated and expanded version of the Macromedia Mach-II Development Guide originally written by Sean Corfield. Sean was gracious enough to allow us to take the version he did for Macromedia and update it.
Because of the substantial changes in the newer versions of Mach-II we do not recommend that the original Development Guide is used for anything other than historical purposes. The intent of this document is to supplant the original document for all future versions of Mach-II. Many of the practices outlined in the original document are no longer considered best practices and in some cases functionality outlined in the original Development Guide has been deprecated or eliminated from Mach-II.
This Development Guide is structured as follows:
- Concepts and Core Files
- Application Structure and General Design
- Designing Models for Mach-II
- Designing Views for Mach-II
- Designing Event Handlers for Mach-II
- Designing Event Filters for Mach-II
- Designing Plugins for Mach-II
This section explains the basic concepts behind the Mach-II framework and describes how to access the core files.
Model-View-Controller (MVC) is the design pattern that forms part of the basis of the Mach-II architecture. In MVC, the presentation (View) is completely separated from the business logic (Model), and the interaction between the Model and the View is handled by Mach-II, which serves as the Controller. The specific CFML constructs applicable to each layer of the MVC architecture are as follows:
- Views are .cfm files that render HTML (presentation code) and make use of the Mach-II Event object. An individual instance of the Event object is created by Mach-II for each event announcement (see below for details).
- The Model is a set of ColdFusion Components (CFCs) that represent all the business logic of your application, and these CFCs contain functionality such as the database interaction and business rules of the application. The model objects are not and should not be aware of Mach-II, nor should they interact with Mach-II objects directly. This ensures that your Model is as flexible and reusable as possible.
- The Controller is, essentially, Mach-II itself. Specifically, index.cfm serves as a front controller, and in conjunction with the mach-ii.xml configuration file and Mach-II Listeners, these items comprise the Controller layer of the application. Each of these elements are discussed in detail below.
An important part of the Controller layer in a Mach-II application are Listeners, which are CFCs that extend the framework's base Listener object and provide the connectivity between the front-end (View) and back-end (Model) of the application. Listeners are also part of the flow control mechanism of the application.
Listener CFCs should always be considered part of the Controller layer, not the Model layer. This is an important distinction to bear in mind as you build Mach-II applications. The Model should always be built in such a way that it is completely unaware of the Controller layer and the Mach-II framework. This ensures that your model is reusable independent of Mach-II, and also that your business logic can easily be used by external technologies such as Web Services, AJAX, remote object calls from Flex, or even another MVC/HTML framework. See "Designing Models" below for additional information.
In addition to being based on the MVC design pattern, Mach-II is also based on an Implicit Invocation architecture, which is represented by the "II" in the name Mach-II. Please refer to the excellent article entitled An Introduction to Implicit Invocation Architectures by Ben Edwards (creator of Mach-II) for more information.
The basic premise behind implicit invocation is that rather than one page within the application linking directly to another page by a hard-coded URL, each action in the application is an event announcement and during the course of this event various actions occur. Events can be announced either by the user (e.g. a button is clicked) or from within the application code itself (e.g. form processing is complete), and these events are handled by event handlers that are defined in the mach-ii.xml configuration file. The event handlers respond to event announcements by performing business logic and then either presenting a view to the user or announcing another event. Mach-II queues the announced events and processes them, invoking methods on listeners or performing other actions as defined in the event handlers, and this process continues until all the events in the queue have been processed.
This document makes reference to several design patterns, such as Session Facade, Memento, Transfer Object, and others. Here are some good references on design patterns:
- Sun's Core J2EE Patterns Catalog (includes Composite View, Session Facade, Transfer Object): http://java.sun.com/blueprints/corej2eepatterns/Patterns/index.html
- Martin Fowler's Catalog of Patterns of Enterprise Application Architecture (including Model-View-Controller, Gateway): http://martinfowler.com/eaaCatalog/
- Portland Pattern Repository's Wiki - Pattern Index (including Memento and many others): http://c2.com/cgi/wiki?PatternIndex
- Head First Design Patterns by Freeman, Freeman, Bates, and Sierra (O'Reilly): http://www.oreilly.com/catalog/hfdesignpat/index.html
The Mach-II framework is itself a CFML application, and is simply a collection of ColdFusion Components (CFCs) that are stored in a MachII directory in the web root or a mapped equivalent. A single group of core files can be shared by multiple applications on a single server. In other words, each application that is using Mach-II does not need an independent copy of the framework, though if you choose to have your applications 100% isolated from other applications on the same server, you can certainly include the Mach-II core files in your application's docroot. The version of Mach-II being used in a production environment should be stored in the source code control system (e.g. Subversion) so that all developers agree upon and have access to the same version of the Mach-II core files.
Mach-II applications use Application.cfc or Application.cfm and index.cfm differently than traditional CFML applications. Mach-II is a front-controller framework, meaning that all requests to the application are routed through index.cfm. Because of this fact, index.cfm will not contain any code that is displayed to the user, and when Application.cfc is used, index.cfm will actually be an empty file.
The use of Application.cfm is more or less traditional in that it defines the application name and other application settings, such as whether or not to enable session management, etc. Where the usage differs is that for the most part, application-wide variables will be defined as Mach-II properties as opposed to being set in the Application scope inside Application.cfm. Additional details about Mach-II properties will be provided below.
In a Mach-II application that uses Application.cfm, the index.cfm file includes the mach-ii.cfm file that is part of the Mach-II core:
<cfinclude template="/MachII/mach-ii.cfm" />
This allows Mach-II to service all of the requests that come into the application through index.cfm. Some additional settings are included in index.cfm; these are discussed below.
Using Application.cfc is recommended in Mach-II applications that will be running on Open BlueDragon, Railo, New Atlanta BlueDragon 7 or higher, and Adobe ColdFusion 7 or higher. (Note that versions of Adobe Coldfusion prior to 7 are not supported.)
An application's Application.cfc must extend the mach-ii.cfc that is included in the core Mach-II files:
<cfcomponent displayname="Application" output="false" extends="MachII.mach-ii">`
When using Application.cfc, the inclusion of mach-ii.cfm in index.cfm
outlined above is instead handled by Mach-II's handleRequest()
method.
The call to Mach-II's handleRequest()
method should be placed in the
onRequestStart()
method in Application.cfc. In addition, a call to
Mach-II's loadFramework()
method is placed in the onApplicationStart()
method in Application.cfc. Other variables discussed below are set as
variables in the default constructor area of Application.cfc.
Starting with Mach-II 1.5, the MachII.mach-ii CFC includes numerous intelligent default settings so you should not have to worry about as many configuration settings as in older versions of Mach-II. Mach-II 1.6 and 1.8 are continuing this trend.
Note: With the use of Application.cfc, an empty index.cfm file is still
required; otherwise, 404 errors would be thrown due to the lack of an
index file. Also note that although the output attribute of
Application.cfc as a whole is set to false, the output attribute of the
onRequestStart()
method must be set to true, because the page output
occurs during the execution of this method.
Values for MACHII_CONFIG_MODE
, MACHII_CONFIG_PATH
and, if needed,
MACHII_APP_KEY
can be customized for each application. In index.cfm
this is done prior to including mach-ii.cfm, or in Application.cfc these
variables are set in the default constructor area of the CFC. By
default, the mach-ii.cfm core file uses the directory in which the
request originated as the key in the application scope in which the
application's instance of the framework objects are stored. This can be
overridden by setting `MACHII_APP_KEY" in index.cfm or in
Application.cfc, but it is not generally recommended to change this
value.
By default, mach-ii.cfm looks for ./config/mach-ii.xml
to use as the
application's configuration file, which means that Mach-II will look for
the mach-ii.xml file in a config directory relative to the application
root, not the web root. The mach-ii.xml file is used to initialize the
framework for the application and load the application's Mach-II-aware
objects such as Listeners, Filters, and Plugins into the Application
scope. The path to the XML configuration file can be overridden by
setting MACHII_CONFIG_PATH
in index.cfm or Application.cfc.
For security purposes, it is recommended that the mach-ii.xml file be
placed outside the web root so that it cannot be accessed through a web
browser, in which case the MACHII_CONFIG_PATH
variable must be updated
to reflect the correct location of the mach-ii.xml file. Alternatively,
the file may be left in the config directory but should be renamed
mach-ii.xml.cfm so that the raw XML file cannot be browsed directly. If
the file is renamed with a .cfm extension, the following line should be
placed at the top of the file in order to prevent displaying the
contents of the file in a web browser:
<!-- <cfsetting enablecfoutputonly="true" /> -->
This will allow Mach-II to parse the file correctly as XML, but the CFML engine will prevent the contents of the file from being viewed in a web browser.
MACHII_CONFIG_MODE
tells Mach-II when it needs to reload the
framework, which also reloads the files within your application that are
configured and cached by the framework, i.e. Listeners, Event Filters,
and Plugins (more details below). The configuration settings behave as
follows:
"1" (always)--the application is reloaded on every request.
Pros: changes to objects managed by Mach-II (Listeners, Filters, and
Plugins) are picked up immediately.
Cons: slow, because the framework and objects that extend framework
objects are reloaded on every request. This can also hide subtle bugs,
because if the listener stores data in the variables scope, it will
behave as data in the request scope since the listener is recreated for
each request.
"0" (dynamic)--the application is reloaded only when the timestamp of mach-ii.xml changes.
Pros: increased speed, and the listener instance data behaves correctly
(i.e. persists from request to request). In addition, it is easy to
force a reload by changing the mach-ii.xml configuration file.
Cons: changes to listeners are not automatically picked up; the XML
file must be changed in order for the framework to pick up listener,
filter, and plugin changes, because Mach-II configures these object
types for you and stores them in the Application scope.
"-1" (never)--the application will never be reloaded unless Coldfusion is restarted , the application times out, or a reload is forced programmatically.
Pros: increased speed, and creates a stable production environment because builds can be deployed to productions servers without changing the application behavior until the application is restarted.
Cons: in order for changes to be picked up by Mach-II, ColdFusion must be restarted, or the configuration mode must be changed from -1 to 0 or 1, and then changed back to -1 to put the application back in production mode.
Note that for things such as views, which are not automatically cached or placed in the Application scope by Mach-II, a reload is not required for changes to take effect. Model objects themselves are not aware of Mach-II and therefore would also not necessitate a restart when changed but in practice, instances of Model objects are often created within Listeners, and if this is the case, the application would need to be reinitialized in order for changes to the Model object to be recognized.
A setting of "0" is good for general development because the application
will only reinitialize when necessary, which offers improved performance
while developing since the entire application does not reload on every
request as it would with a configuration setting of 1. In addition, if
application state is important during development, having the
application reload on every request can cause issues. Note, however,
that if a Listener, Plugin, or Filter is changed, and the
MACHII_CONFIG_MODE
is set to 0, the mach-ii.xml file will need to be
updated and saved so the timestamp on the file changes. Otherwise any
changes to objects managed by Mach-II will not reload.
With the release of the Mach-II Dashboard, individual application components may be reloaded by clicking a reload button.
It can also be useful to add the following code or something similar to
index.cfm, or to wrap the handleRequest()
method call in the
onRequestStart()
method of Application.cfc in this (or similar) code:
<cfif not request.productionMode and structKeyExists(url, "reloadApp")>
<cfset MACHII_CONFIG_MODE = 1 />
</cfif>
This allows the application to be reinitialized easily during
development by adding ?reloadApp to the URL. In production,
MACHII_CONFIG_MODE
will typically be set to -1, meaning it will only
reload if the CFML engine or Java web application is restarted or if the
Application times out.
This section describes how to structure Mach-II applications and provides some general design guidelines for building object-oriented applications in CFML using Mach-II.
This section explains how to structure applications for Mach-II and contains recommended standard directory structure guidelines.
As discussed in the "Mach-II Core Files" section above, this is the central configuration file for a Mach-II application that specifies all the listeners, events, views, etc. for the entire application. Think of this file as an easy-to-reference roadmap for the entire application. As mentioned above, it should live outside the document root for security purposes, because it may contain sensitive information such as datasource names that should not be exposed to end-users of the application.
The Mach-II application skeleton contains directoris for filters, listeners, model, and plugins. This is the minimal directory structure that should be used in order to separate the CFCs by type or concern in an application. If this directory structure is used, please ensure that your Listeners are not considered to be part of the Model layer of your application, and that the model directory contains only business logic CFCs.
The directory structure of the Mach-II application skeleton is not required by Mach-II, and a different directory structure adhering to a more standard Java package structure (reverse domain name structure) is recommended. For example, the following would represent a directory structure for a simple contact management application that deals with person-related data:
{web_server_root}
appname/
Application.cfc
index.cfm
config/
mach-ii.xml or mach-ii.xml.cfm (this may also be placed elsewhere as outlined above)
com/ (this is the root of the Model layer)
mydomain/
appname/
person/
Person.cfc
PersonDAO.cfc
PersonService.cfc
customtags/ (use custom tags for view-related or presentation logic)
filters/
listeners/
PersonListener.cfc
plugins/
views/
home.cfm
login.cfm
MachII/ (Mach-II framework files)
See the "Coding Style Guidelines" document for additional details concerning application directory structure.
Rather than lumping CFCs of different types or with distinctly different concerns in a single model directory, CFCs should be separated and grouped by their function, e.g. listeners, filters, plugins, etc., and the model should further be contained in a reverse domain name directory structure similar to the style of organizing Java packages. Again, the model objects should know nothing about Mach-II, and Mach-II listener objects should not be considered as part of the model layer of your application.
This section provides guidelines for designing a Mach-II application. General guidelines will be presented in this section, and subsequent sections will address the specifics of designing models (including some basic design patterns), views, event handlers, event filters, and plugins.
The design of a Mach-II application can be separated into two high-level areas of concern: the business model, and the user interface (UI). In most cases it is possible to perform a certain amount of the design of these two areas in parallel. As will become clear in the sections below, the business model components can be designed and implemented independently of the rest of the application, and should also be implemented independently of the Mach-II framework itself.
The UI can also be designed and implemented independently of the rest of the application. The actions taken by the users in the UI drive the design of the event model in a Mach-II application, and this in turn drives the design of the event handlers and listeners, constrained by the interface of the business model. In teams on which the UI work and backend engineering work are handled by separate individuals or groups, this allows for more efficiency early in the project lifecycle. Furthermore, the loose coupling encouraged by Mach-II allows for more flexibility and easier maintenance later in the lifecycle. Mach-II applications are amenable to change by their design.
For many developers, especially those new to object-oriented design, the most difficult part of building an application with OO principles is determining which components are needed. One approach is to write a short, coherent narrative that describes the interaction in the business logic of the application. Most of the nouns in such a narrative suggest themselves as components, and most of the verbs suggest themselves as methods on those components.
Remember that not everything needs to be a component. Some things simply aren't important enough in the overall business model to require a component to model their behavior. In one application a person's name migth be important enough to be modeled as a component that understand title, greeting, first name, last name, middle name or initial, suffix, etc., whereas in another application it may suffice to have a string for first name and another string for last name. There are no hard and fast rules.
Once components are identified, the next step is to consider how well defined they are. Each component should be highly cohesive, meaning it should do (or represent) one thing only and do it well. The components should also be loosely coupled, meaning they should not need to know about or depend upon one another. Be prepared to refactor code throughout the development process in order to improve cohesion and reduce coupling. The first choice of components may be obvious, but it won't necessarily be the best representation of the business logic.
If it is difficult to represent a relationships between two or more components, the relationship between the components itself may be important enough to warrant its own component. In some cases the relationship between components may actually be more important than the components themselves. Also remember that a relationship between two components represents a coupling between those components. Abstracting the relationship into its own component can often reduce the coupling between the components, thereby providing more flexibility.
The other key point to consider in object-oriented design is inheritance versus composition. Many OO novices see inheritance everywhere and try to have components extend other components as a rule. In reality, inheritance is a very special relationship between components: it defines an "is-a" relationship. Inheritance should not be terribly common, and it is also a form of very tight coupling between the parent and child components, so it should not be used unnecessarily. Unless the relationship genuinely represents an immutable "is-a" relationship, where a child component instance is wholly substitutable for the parent component instance in every use case, then you should consider some form of composition instead.
Erich Gamma, one of the authors of Design Patterns: Elements of Reusable Object-Oriented Software http://www.awprofessional.com/title/0201633612, also known as the "Gang of Four" design patterns book, advises to "favor composition over inheritance." Composition can be broadly described as a "has-a" or "refers-to" relationship. A good example of this is the Employee / Manager model. Novices often try to model this with inheritance because a Manager "is-an" Employee, but this model breaks down quickly when promotions and substitutions are taken into account. A more accurate model is that an Employee "has-a" Role, and Manager "is-a" Role. When a regular employee is promoted, it is only really their role that changes and, in some companies, an employee can actually have multiple roles, especially if the company has a matrix reporting system or bestows temporary roles on employees.
Good object-oriented design takes time and practice, so leverage the work of others by learning about and using design patterns, which are really nothing more than proven solutions to common OO modeling issues.
This section discusses design issues in the Model layer of a Mach-II application. The Model encompasses all of the business logic in your application and is implemented as a collection of Coldfusion Components (CFCs).
Mach-II interacts with the Model portion of an application using components that extend the MachII.framework.Listener CFC. In a very simple application, business logic might be implemented in these listener objects. This approach is not recommended, however, because it does not scale well with increasing application complexity. Among other issues, placing business logic in listener objects introduces a tight coupling between the elements of the business logic and the Mach-II framework. If the business components are carefully designed based on the guidelines in the previous section, it should be clear that only a few of those components are natural listeners for events. None of your basic business components are natural listeners. This further illustrates the fact that your listener objects should not be considered as part of the Model layer of your application. Keeping the listeners completely free of business logic is a desired goal. The logic contained in the listeners should be limited to dealing with things such as, for example, preparing form data for submission to the Model layer. Functionality such as applying business rules or communicating with a database should never appear in your listeners.
It is best to ensure that there is only a thin layer of coupling between the framework and the business logic of the application. If necessary, create new components that extend MachII.framework.Listener and whose sole purpose is to communicate Mach-II events to the components within the business model. In other words, try to isolate the business components from the framework components as much as possible following these general guidelines:
- Only listener components should know about the Mach-II framework
- Only listener components should access Mach-II properties
- Only listener components should announce events
The core business components should know nothing about Mach-II and should have no dependencies on the framework whatsoever.
In the vast majority of applications it will likely make sense to create a service layer to sit between the Mach-II listeners and the core business components. This abstracts the business logic from Mach-II even further, and also provides a convenient API through which something other than Mach-II (e.g. Flex, web services, AJAX, etc.) can access the business logic of your application. In short, put as little logic in your listeners as possible and create an API-based service layer between the listeners and the true Model layer of your application. This ensures maximum abstraction and reusability of your business logic components.
The following is an example of a minimal Listener CFC that uses the EventInvoker
invoker type (which is the default invoker type as of Mach-II 1.1.0):
<cfcomponent extends="MachII.framework.Listener" />
<cffunction name="configure" access="public" output="false" returntype="void">
<!--- perform any initialization --->
</cffunction>
<cffunction name="someMethod" access="public" output="false"
returntype="[SomeType?](/machii/wiki/SomeType)">
<!--- take the Mach-II event object in as an argument so we have
access to URL and form data --->
<cfargument name="event" type="MachII.framework.Event" required="true" />
<!--- perform additional tasks here as needed, then return result --->
<cfreturn instanceOfSomeType />
</cffunction>
</cfcomponent>
The configure method of the listener is invoked once (and only once) when the application first initializes, and this is handled automatically by Mach-II. Any dependencies, instance variables, or other tasks related to the initial setup of the listener should be placed inside this configure method.
The method someMethod()
can be invoked from an event handler declared in
mach-ii.xml by using the <notify>
command. In the example above, the
returned value instanceOfSomeType should be of type SomeType
. A return type of any should only be
used if the method truly requires a dynamic return type. If you know the
data type that should be returned by the method, whether that be a
native Coldfusion datatype such as a query
or a custom data type as in the example above, that type should be
declared as the return type in the method definition. Note that the
return type can be set to void if the method does not return anything.
A listener is declared in mach-ii.xml as follows:
<listener name="listenerName" type="Path.To.YourListener" />
This imples that the default EventInvoker
invoker type will be used, which places all the URL and form variables
into an instance of MachII.framework.Event. This object is then passed
to the invoked listener method as a single argument, making the event
data available to listener methods. This is typically the desired
behavior in a Mach-II application.
If a different invoker type is required, it is declared as follows:
<listener name="listenerName" type="Path.To.YourListener">
<!--- declaration of invoker type --->
<invoker type="MachII.framework.invokers.EventArgsInvoker" />
</listener>
The <invoker>
tag specifies how the methods are invoked on the
listener. In the example above, the EventArgsInvoker
type that ships with
Mach-II is used. This invoker passes URL and form variables to listener
methods as individual named arguments as opposed to placing them all in
an instance of MachII.framework.Event. See the "Invokers and Listeners"
section that follows for more details.
Note that the older CFCInvoker_Event.cfc
and `CFCInvoker_EventArgs.cfc"
have officially been deprecated and will be removed from a future
version of Mach-II.
You may optionally specify parameters within the listener declaration:
<listener name="listenerName" type="Path.To.YourListener">
<parameters>
<parameter name="param1" value="value1" />
<parameter name="param2" value="value2" />
</parameters>
</listener>
This provides default parameter values for param1 and param2 of value1
and value2 respectively. These can be accessed within the listener using
the getParameter()
method, e.g. getParameter("param1"). The listener
inherits this method from the MachII.framework.Listener base class.
The methods of a listener CFC are invoked in an event handler as follows:
<event-handler event="someEvent" access="public">
<notify listener="listenerName" method="someMethod" resultArg="someVariable" />
</event-handler>
This causes the someMethod()
method to be called within the context of
the current event, and the returned result from the call to someMethod()
is stored in the someVariable variable that is put into the Mach-II
event object. This variable is then accessed from the event object using
the event object's getArg()
method. In this example, you would access
someVariable from the event object by calling
event.getArg("someVariable")
. Note that the resultArg attribute is
optional and should not be specified for methods with a return type of
void.
In versions of Mach-II prior to 1.1.0, resultKey was used as opposed to resultArg, and often the request scope was often used as a data bus instead of using the event object. This syntax is still supported through the use of the old invokers, but is not supported by the new invokers. Remember that the old invokers have been deprecated and will be removed in a future version of Mach-II, so other than situations in which an older version of a Mach-II application is being deployed on a newer version of the Mach-II framework, the old invokers should not be used. It is our recommendation that you update all old code to take advantage of the new invokers and new resultArg syntax.
A listener only has access to the current event, but additional events
can be added to the event queue by calling announceEvent()
from within
the listener. Events added to the queue are executed at some point after
the current event has been handled. A listener method typically calls
one or more methods on one or more business model objects. The business
model objects might be created on the fly, such as with something like
an instance of a bean; they might be managed in a shared scope such as
the application or session scope; or they might be created within the
listener's configure()
method (which is called automatically by Mach-II
when the application first initializes) and then stored in the
listener's variables scope.
For more information on writing listeners, refer to Intro to Mach-II Listeners
When a listener is declared in mach-ii.xml, if an invoker type is not
specified the default EventInvoker
is
used. As explained above, the EventInvoker
passes the current event object to the invoked method as a single
argument. In versions of Mach-II prior to 1.1.0, declaring an invoker
type for each listener was required. Declaring an invoker type is now
only required if using an invoker other than the default
EventInvoker
, which is typically not
necessary.
The invoker affects how arguments are passed to the listener methods in
a <notify>
tag, and also affects what happens to the data returned
from the method called. As noted above, the default EventInvoker
causes the whole event object
to be passed as a single argument of type MachII.framework.Event to the method, whereas the EventArgsInvoker
causes all of the event arguments to be passed to the listener method as
separate named arguments. Both invokers store the result of method calls
in the variable specified in the resultArg attribute of the <notify>
command, and this variable is in turn put into the event object. In the
majority of cases the default EventInvoker
will be used, but there are some issues to consider:
-
EventArgsInvoker
may seem to provide better type safety since the type of each argument, whether or not each argument is required and, if not, whether or not the argument should have a default can all be specified. However, letting the CFML engine itself validate what in practice amounts to the URL and form data is not very robust. Particularly for data that will be put into the application's database, more robust data validation is necessary. -
EventInvoker
provides more flexibility, but at the expense of requiring additional code inside your listener method to access and validate the event arguments. In the case of theEventArgsInvoker
, each event argument is accessed as arguments.argName. TheEventInvoker
encapsulates individual arguments into a single event object, and the event arguments are then accessed via thegetArg()
method of the event object. For example, to access an argument called argName, the following method call would be used:arguments.event.getArg("argName")
- In general, use the default
EventInvoker
because it provides a great deal of additional convenience, and consider using event filters (more details on event filters are provided below) or other type checking to enhance the type safety of your application.
When the EventInvoker
is used, the
listener method is passed a single argument that should be declared as
follows:
<cfargument name="event" type="MachII.framework.Event" required="true" />
In order to test whether or not a given URL or form variable was
provided, use the isArgDefined()
method on the event:
<cfif arguments.event.isArgDefined("anArg")>
...
</cfif>
Since URL and form variables are inherently strings as far as HTTP is
concerned, it is better to explicitly validate the event argument types
in the code itself rather than using EventArgsInvoker
and declaring method
arguments for individual URL or form variables. The CFML engine will
attempt to (and typically does a good job) of type converting form and
URL variables, but if for some reason the type cannot be converted, for
example, from a string to a numeric data type, the CFML engine will
throw an error when it encounters the method argument containing the
wrong or inconvertible data type. This gives you less control over
validation and error handling.
Custom invokers may be created if an application requires behavior other than that which exists in the invoker types that are provided with the framework.
Since Mach-II loads all the framework component instances (i.e. listeners, filters, and plugins) into the application scope when the application is initialized, bear in mind that any instance data created in a listener will effectively be stored in the application scope. This has three main implications:
- Pro: You can cache data for the application very easily by storing it as instance data within a listener's variables scope.
- Con: Updating instance data affects all threads and should therefore
be locked appropriately. Use
<cflock type="exclusive" name="..."> ... </cflock>
around any updates on data in the variables scope, with an appropriately chosen lock name. - Con: To manage per-session data, implementing some sort of Session Facade is desired. See below for additional details.
In general, listeners should be stateless, meaning they should contain
no instance data unless data is specifically being cached for
performance reasons. One example of this is saving property values in
the variables scope to avoid accessing the properties dynamically in
each request. Consequently, extra care must be taken to use var to
declare all local variables in listener methods so that variables aren't
unintentionally stored in the unnamed scope. Remember that tags such as
<cfquery>
create variables too, so these must be var scoped as well:
<cfset var userSelect = 0 />
<cfquery name="userSelect" ...>
...
</cfquery>
As mentioned above, if per-session data is used within the application, the Session Facade design pattern will be used. By using a Session Facade pattern, only the listener is session-aware, meaning that the listener knows about the session scope and manages component instances that live in session scope, but per-session component instances are not listeners themselves and therefore should not reference the session scope directly.
For example, a shopping cart listener would respond to events such as addItem, removeItem, and updateQuantity, but it would delegate the actions to a cart object that is stored in the session scope. The shopping cart listener would create the cart object in the session scope on demand. The cart object would store information as instance data (which is per-session because the cart is per-session) but would not reference the session scope.
<cfcomponent displayname=”CartListener” extends="MachII.framework.Listener">
<cffunction name="addItem" ..>
<cfargument name="item" ../>
<cfset getCart().addItem(arguments.item) />
</cffunction>
<cffunction name="getCart" returntype="Cart" access="private">
<cfif not structKeyExists(session,"cart")>
<cfset session.cart = createObject("component","Cart").init() />
</cfif>
<cfreturn session.cart />
</cffunction>
</cfcomponent>
The partial example above demonstrates the Session Façade technique but
is not intended to be production quality; for example, there's no
configure()
method for the listener, there's no hint= or output=
attributes, it doesn't lock session scope when creating the cart object
which might be needed if you are concerned about threaded access for a
single user (which may or may not be a concern depending on the
application).
The basic event lifecycle in Mach-II is as follows:
- An event is announced
- Listeners are notified and subsequently return data to the event object as a resultArg
- A view is rendered that utilizes and/or displays the event data, or another event is announced
When more than one piece of data is needed by a view, the listener could be notified multiple times, creating several resultArgs that are subsequently put in the event object that is available to the view. This may seem like the obvious approach, but there are several negative aspects associated with this approach:
- Multiple calls are made to methods on the listener, causing possible performance issues
- A very granular interface is required within the listener, meaning lots of low-level getters
- Creates complex dependencies between views and input variables, because numerous separate variables within the event object are required
These issues should raise red flags, particularly the last two, which cause encapsulation to break down and coupling to increase. There is nothing wrong with notifying multiple listeners in a single event, or even the same listener multiple times, if it’s truly necessary. When the data is related, however, the Transfer Object pattern provides a better solution to this problem.
When is this pattern likely to occur within an application? A good
example is a view that displays information about a person. The view
probably needs to display first name, last name, street address, city,
state, zip, and so on. The obvious (but naïve) approach would be to have
getter methods for each of these pieces of data within the
listener - which necessitates separate calls for each of these listener
getter methods in the event handler - and have the view depend on
event.getArg(“firstName”)
, event.getArg(“lastName”)
,
event.getArg(“streetAddress”)
, and so forth. In addition to the problems
outlined above, this is not a particularly elegant solution.
The Transfer Object design pattern is intended for situations such as
this, and it solves the problem by aggregating data into a single object
that is passed between the model and the view. In the above example, a
single getPerson()
method would return a single struct that contains all
of the data needed, and the view would then have a single dependency on
that one event argument. For example, if the getPerson()
method in the
listener returned a struct that was put in a resultArg called person,
this struct would be available to the view by calling
event.getArg(“person”)
.
If greater encapsulation is desired or functionality beyond what a
struct can provide is required, or if an application employs a complete
object model, beans are used as the Transfer Object. A bean is a simple
CFC containing getters and setters for the data as well as a constructor
(init()
) that the listener component uses to set all the data in the
transfer object before returning it to Mach-II. Beans are extremely
common in OO applications. See the “Beans and Form Handling” section
below for additional information about beans.
As indicated above, a bean is a simple CFC with getters and setters that
encapsulates the data (a.k.a properties or attributes) within the bean.
Beans are typically used as Transfer Objects to pass data between
different layers in an application. If a bean has a property foo (a
private instance variable), then it also has the methods getFoo()
and
setFoo()
to get and set, respectively, the value of foo. The getFoo()
method will be public; the setFoo()
method may be public or private
depending on whether the bean is considered read-only or read-write.
Here is a simple read-only bean:
<cfcomponent>
<!--- declare properties for clarity: --->
<cfset variables.foo = "" />
<!--- constructor: --->
<cffunction name="init" returntype="FooBean" access="public" output="false">
<cfargument name="foo" type="string" default="" />
<cfset setFoo(arguments.foo) />
<cfreturn this />
</cffunction>
<!--- public getters: --->
<cffunction name="getFoo" returntype="string" access="public" output="false">
<cfreturn variables.foo />
</cffunction>
<!--- private setters: --->
<cffunction name="setFoo" returntype="void" access="private" output="false">
<cfargument name="foo" type="string" required="yes" />
<cfset variables.foo = arguments.foo />
</cffunction>
</cfcomponent>
A read-write bean differs from the above example only in that the
setters are public. The constructor has an optional argument for each
property and calls setXxx()
for each property xxx.
When a form is submitted within a Mach-II application, Mach-II supports
bean creation and population through the <event-bean>
command:
<event-handler event=”processForm” access=”public”>
<event-bean name="beanName" type="beanType" fields="field1,field2" />
</event-handler>
This command creates a bean of the specified type (beanType, e.g., my.model.Foobean
and stores it in the current event object as an event argument called beanName (e.g., fooBean or just
foo).
If fields= is specified, the <event-bean>
command calls the bean’s
constructor (the init()
method) with no arguments and then calls the
setter for each field specified in the list of fields. Using the
<event-bean>
example above, this would create a bean of type beanType,
call the bean’s init()
method, and then call the setXxx()
method for
each field and pass it the appropriate data from the form post. In this
case the set method calls would be as follows:
setField1(event.getArg("field1"))
setField2(event.getArg("field2"))
Note that for the event bean to be populated properly, the field names
in the form must match the property names in the bean. For example, if a
Person bean has the properties firstName and lastName, and an
<event-bean>
command is used to populate a Person bean with form data,
the form fields must be named firstName and lastName in order for the
form field data to be used to automatically populate the bean. Also note
that there must not be spaces between the list of field names in the
event bean command (fields=”field1,field2” NOT fields=”field1, field2”).
If fields= is omitted, the <event-bean>
command calls the bean’s
constructor with all the current event’s arguments by name (e.g.,
theBean.init(field1=event.getArg("field1")
,
field2=event.getArg("field2")
).
The <event-bean>
command makes it very easy to handle form submissions
in Mach-II. Simply define a bean component to represent the data in a
form, ensure that the form fields and bean property names match one
another, and use the <event-bean>
command to populate the bean using
the form data that is submitted. The submitted data can then be operated
upon via an encapsulated bean, which allows for performing validation
(see “Designing Event Filters” below), persistence, or whatever
additional functionality is required.
For more information on writing and using beans, refer to the article Beans, Beans, the Musical Fruit.
Although this topic is not directly related to the Mach-II framework, most applications need to implement data access, typically to a relational database, so providing guidance on best practices within the context of Mach-II does not seem beyond the scope of this document.
Note that in the original version of this guide it was recommended to split this functionality into a separate DAO and Gateway objects. THIS IS NO LONGER RECOMMENDED. This artificial splitting of database-related functionality into two separate CFCs adds unnecessary complexities and is to be avoided.
There are two basic patterns of access to persistent data within most applications:
- Aggregated access: typical in reporting, searching, or any process that retrieves multiple rows from the database
- Per-object access: used for creating, editing, and working in depth with a single row of data
CFML has an excellent built-in idiom for dealing with aggregated data, namely the query object, which provides an efficient way to manipulate potentially large sets of data retrieved from a database or other data source. When dealing with aggregated data it usually does not make sense to convert every row returned into a fully encapsulated object, because the typical use case for aggregated data is to display tabular results to the user, or serve as the "master" list in a master-detail scenario. Using an object for each database row in these situations adds unnecessary overhead and complexity to the process, so unless there is a specific need to use a collection of objects, use the query object for aggregated data.
When dealing with what corresponds to a single row in a query, however, it usually does make sense to interact with a fully encapsulated object, since the interest at this level is in working with a specific object and its data. It is in dealing with individual records from the database that the standard Create, Read, Update, and Delete (CRUD) methods begin to appear.
The difference between returning a query object for aggregate data and returning a bean can be illustrated with a couple of examples:
- Given a business model object called Order, an OrderDAO component for both per-object access and aggregate data would be created.
- Aggregate data methods such as findAll(), findWhere(), and findByID() would all return standard query objects (even findByID() which may return a single row).
- CRUD methods such as create(), read(), update(), and delete() deal with single records, although it is not required the methods be named this way. (It is not uncommon, for example, to combine the create() and update() methods into a single save() method that will create a new record or update an existing record as needed.) These methods would operate on a specific Order bean as opposed to a query object, exchanging data via the getters and setters in the Order bean, or via some other type of snapshot of the Order's data.
To expand on the final point above, the Order bean could also implement
methods such as getSnapshot()
and setSnapshot()
that would interact with
a lighter-weight representation of the bean's data, such as a struct or
some other construct implementing the Memento design pattern. This data
is less encapsulated but could provide improved performance when
necessary, and might also serve as an easier conduit between the CFML
engine and a web service when the full bean data type is not necessary.
Separating these operations from the business model objects themselves helps the business objects remain persistence-neutral, so that if the method by which data is persisted ever changes, the business objects themselves will not need to be altered. The DAO components can be optimized for retrieving large record sets, caching, etc. for aggregate data, as well as dirty data updates, pooled object access, and so on for single record access.
One alternative to the DAO pattern is the “active record” pattern that
is probably best known from its implementation in Ruby on Rails. In the
active record pattern the business model objects themselves would
contain CRUD methods, meaning that they would know how to persist
themselves. Many Coldfusion
Object-Relational Mapping (ORM) frameworks such as Reactor and Transfer
use the active record pattern, whereby all of your business objects
extend a single object from the ORM framework, and the base ORM object
provides the necessary persistence methods such as read()
, save()
, and
delete()
.
The pros of implementing CRUD in your business model object are:
- Fewer components to keep track of; no separate DAO components
- No need to implement data transfer machinery; the CRUD methods will have direct access to the data within your business model object
The cons are:
- SQL is mixed in with your business logic in the same component, which removes encapsulation of the persistence layer itself.
- The business model object is heavier, more complex, and less cohesive.
- Makes it more difficult to change the persistence layer to use a different data source or completely different persistence mechanism altogether, because the persistence mechanism is coupled to the business model object. Having a separate DAO layer allows you to persist some business model objects one way while persisting others in a different way, or to easily change the persistence mechanism altogether.
It is the driving forces of good encapsulation, high cohesion, and loose coupling that lead to the recommendation above. Providing DAO components that are separate from the business model components creates maximum flexibility in the business model components, and also reduces coupling between business model objects and the persistence layer of the application.
This section discusses design issues in the View layer of a Mach-II application. The View encompasses all of the HTML user inteface (UI) for a Mach-II application and is implemented as a collection of .cfm pages.
Views should not contain any logic other than presentation-specific
logic. As an example, a view should never contain a <cfquery>
, but
logic such as looping over a query for display purposes will obviously
be contained in views. Dynamic data required by a view should be passed
in from the controller (Mach-II) via the event object, which is
accessible from a view by referring to event. The section covering
Transfer Objects above touches on this topic and recommends that views
depend on as few variables as possible to maintain encapsulation and
reduce coupling.
An important point in this discussion is that the arguments in the event
object that are used by a view define the API between the controller and
the view, and thus specify a contract that should be agreed upon early
in the design, documented, and then honored for rest of the project
lifecycle if possible. This helps maintainability and stability. There
is no way around the fact that the view must call data by name from the
event object (e.g. event.getArg(“myData”)
), so any changes to the event
object’s variable names (i.e. the resultArg declaration in the notify
command, or variables that are explicitly put in the event object
programmatically) will require a corresponding change in the views that
use this data.
A good use of event arguments in a view is when <event-arg>
is used to
set values for parameterized views, such as forms that support both edit
and create operations. This is a better approach than hard-coding the
submit and cancel actions into the view itself.
Fusebox uses a similar technique that is referred to as XFAs (eXit FuseActions). By analogy, these constructs in Mach-II could be described as eXit Events (XEs). The use of XEs reduces coupling between views and the application control flow, so it's good practice to parameterize views where appropriate. In this way, the exit paths out of a view (i.e. links and form actions) are passed in as event arguments.
For example, a paged record set view might have previous / next page links like this:
<a href="index.cfm?event=#event.getArg('XEPrevious')#">Previous</a>
|
<a href="index.cfm?event=#event.getArg('XENext')#">Next</a>
This allows the view to be invoked in different parts of the application
with the previous / next events passed in from <event-arg>
commands
contained in the mach-ii.xml file.
Mach-II allows views to be rendered into contentArg variables so that a single HTML page can be constructed from multiple views or page fragments. These contentArg variables are stored in the event object so they also form part of the API between the controller and the views, as discussed above. The same caveats outlined above therefore apply.
As a general guideline, the application will be more maintainable if dynamic data (i.e. data from the event object) is passed into simple views that are rendered into contentArg variables, and these contentArg variables are then aggregated into a single layout template view. For example:
<event-handler event="showFoo" access="public">
<notify listener="foo" method="getFoo" resultArg="fooData" />
<view-page name="fooPage" contentArg="content" />
<view-page name="mainLayout" />
</event-handler>
In this example, the API between fooPage and the controller is simply
event.getArg(“fooData”)
(which itself might be a struct or object
containing multiple items), and the API between mainLayout and the
controller is simply event.getArg(“content”)
. There is no dependency
between mainLayout and any dynamic data, because all the dynamic data is
rendered into HTML by other views prior to rendering mainLayout.
If several views are combined to create part of a layout, in some cases the event handler can be simplified by appending content to a single contentArg as the event is processed:
<event-handler event="showFoo" access="public">
<notify listener="foo" method="getFoo" resultArg="fooData" />
<view-page name="fooPod" contentArg="content" />
<view-page name="barPod" contentArg="content" append="true" />
<view-page name="mainLayout" />
</event-handler>
In this example, the output of barPod is appended by Mach-II to the output of fooPod within the content variable contained in the event object, so there is no need to use an additional contentArg or different layout.
The desired goal is that each view focuses on rendering only the data on which it depends, and a layout view is used to assemble the finished page from pre-rendered HTML fragments. This keeps each view cohesive, because each view does one job well. This also reduces coupling between the views themselves and between the views and the controller. The next section discusses this principle in a bit more detail.
In a typical application the user interface is quite complex and often has several dynamic elements. The navigation can be dynamic, for example, adapting to the user’s current location within the application. Develop Mach-II user interfaces with an eye towards breaking them down into smaller, simpler parts that form a grid. Most portal sites (e.g., My Yahoo!) are good examples of grid layouts. They have multiple columns, each containing multiple sections that are often referred to as pods.
When developing the UI for an application, implement each of these pods as a separate view that renders event arguments into a contentArg, and use a layout view to assemble the contentArg variables into the finished HTML page. This will produce small, focused, cohesive views that are loosely coupled. This also creates views and user interface elements that are more likely to be able to be reused on other pages or potentially even in other applications.
Views can be reused in Mach-II applications very easily as long as they have only narrow, well-defined dependencies. Even if pods contain only static content, it is still usually better to implement them as views so that you can rearrange page elements more easily through simple changes to the final layout view. This situation is sometimes referred to as “view stacking,” but is really an implementation of the Composite View design pattern; see http://java.sun.com/blueprints/corej2eepatterns/Patterns/CompositeView.html for details.
This discussion raises the question of how best to handle headers and footers. Should they be implemented as separate pod views and assembled by the layout view? If the header and footer are application-wide elements, the chances are that they will only change if the whole look and feel of the application changes. Implementing them as separate views is possible, but would create a situation in which every event-handler that generates HTML looks something like this:
<event-handler event="..." access="...">
...
<view-page name="..." contentArg="content" />
<view-page name="header" contentArg="header" />
<view-page name="footer" contentArg="footer" />
<view-page name="mainLayout" />
</event-handler>
A better alternative to reduce code duplication in mach-ii.xml is to announce the final layout view as an event. For example:
<event-handler event="..." access="...">
...
<view-page name="..." contentArg="content" />
<announce event="layoutPage" copyEventArgs="true" />
</event-handler>
<event-handler event="layoutPage" access="private">
<view-page name="header" contentArg="header" />
<view-page name="footer" contentArg="footer" />
<view-page name="mainLayout" />
</event-handler>
NOTE: With the introduction of subroutines in Mach-II 1.5, using subroutines as opposed to announcing layout events is the preferred method. The guide will be updated to reflect this.
Every event-handler that needs to generate HTML would announce the layoutPage event with the copyEventArgs attribute set to true. The contentArg that is built up in the original event is then passed to the layoutPage event and finally rendered by the mainLayout view.
This latter style is recommended as a general rule because of the reduction of redundant code in mach-ii.xml as well as the flexibility it provides. However, specific situations may dictate a different approach. If the headers and footers are closely tied to the overall page layout, then they can either implemented directly inside the layout view or implemented as separate files that are included in the layout view:
<!--- doctype etc goes here --->
<html>
<!--- head / title etc go here --->
<body>
<cfinclude template="header.cfm" />
<cfoutput>#event.getArg("content")#</cfoutput>
<cfinclude template="footer.cfm" />
</body>
</html>
Custom tags imported as tag libraries may also be used to control the overall rendering of pages:
<!--- set up page settings etc --->
<cfimport taglib="/customtags/view/" prefix="view" />
<view:renderpage>
<cfoutput>#event.getArg("content")#</cfoutput>
</view:renderpage>
NOTE: Mach-II 1.8 introduces form and view custom tag libraries. The guide will be updated to include examples.
By following the recommendations above, views in the application should largely be referencing only the current event object to access data. The event object is effectively the interface between the controller and the view. All of the typical CFML scopes are still available in the view, however, so the following provides some general guidelines as to whether or not other scopes should be accessed directly from views.
- application and server scope: no
Simply from the perspective of encapsulation, views should not reference these scopes; in fact, almost no code should access these scopes directly. Listeners and other parts of the framework are already stored in application scope and therefore can manage application scope data as instance data, and this data can then explicitly passed out via the event object (resultArg) to be made available to views. Server scope should only be used for cross-application caching and should be encapsulated in a listener and, again, passed via the event object to views.
- CGI scope: no
Variables in the CGI scope tend to vary from web server to web server and can be affected by a variety of configuration issues. They may drive the logic in an application but should not directly drive the appearance of an application; therefore, they should not be accessed within a view. If different layouts need to be selected based on, say, the user's browser, this can be managed by using a listener to announce different events based on the CGI variables and handling those events by rendering different layouts.
- cookie scope: no
Since cookies are generally used to drive behavior rather than layout, use of cookie scope inside a view should be avoided. If a cookie's value is intended to be displayed in a view, it should probably be passed through a listener for validation and into the view via request scope or the event object rather than being directly accessed in the view.
- session scope: yes
If an application is tracking data in session scope and a view needs access to that information, it is acceptable to directly refer to session scope within a view. Strictly speaking, encapsulation would oblige the use of a listener or a filter to copy the necessary data into the event object for the view, but in applications that rely on session data extensively this is likely to be too cumbersome in terms of code complexity and may not be worth the extra effort. Not only would this necessitate copying all the necessary session scope data into the event object, thereby expanding the view API, but if operations are performed that might update the data, it needs to be synchronized back into session scope or needs to be accessible by reference rather than by value. If the application has only limited reliance on session scope, a filter can be used to copy session variables to the event object with little overhead. As a general rule for good encapsulation use a Session Façade pattern for accessing the session scope, but accessing the session scope from a view is not prohibited.
- URL and form scope: no
Since data from these scopes is automatically transferred to the event
object, views should never reference URL or form scope directly. Form
data should be handled through a bean that aggregates the form into a
single event argument, either by using the <event-bean>
command or a
filter like the ContactBeanerFilter
in the Contact Manager sample application. Views can retrieve URL and
form scope values from the event object using event.getArg("argName").
This section discusses design issues in the event-handlers section of the mach-ii.xml configuration file. If the recommendations in the “Overall Design Considerations” section are followed, the final design and implementation of both the core business model components and the HTML views is likely complete by the time event handlers are developed. With respect to designing the views, simple interaction schematics should be a sufficient level of detail to enable work on designing the event handlers to begin.
Every action the user may take within the application corresponds to an event. Every link and every form submission is an event, including the default event implied when an event is not explicitly included in the URL. Since the public event names will be a visible, user-facing part of the application, it's worth choosing readable event names of the form verbNoun, similar to the typical naming convention for method names in a CFC. Use mixed case for event names, because it makes them more readable. Event names are not case sensitive in Mach-II so it doesn't matter if a user mistypes such a URL as verbnoun. An acceptable alternative is to use verb.noun, which might be more familiar for developers who come from a Fusebox background since Fusebox uses circuit.fuseaction in the URL.
The events implied by user interactions form the first tier of events in your application. The essence of Implicit Invocation is to abstract the control flow between components into the event model; thus, a large number of events are implied by the application logic. For example, if an event has conditional flow, such as a login event that can succeed or fail, typically event handlers will be created for each of the conditional outcomes. See “Application Neutral Events” below for additional details. Even when the flow is linear, it is often worth separating the initial user event processing from any subsequent application flow such as view rendering.
Event handlers should generally be short and self-contained. Again, this is driven by the desire to have high cohesion and loose coupling. Name the events to reflect this internal partitioning, e.g., sectionVerbNoun or section.verbNoun. As a specific example, an application may have a showMainMenu event for both the public side of the application as well as the administrative side. The public event might simply be showMainMenu, while the administrative menu could be admin.showMainMenu. A prefix such as admin. can also be leveraged in the creation of a plugin to selectively secure various parts of the application.
Although event handlers should be short and self-contained, they should
not be broken up unnecessarily. If there is a sequence of listener
method calls that belong together, use <event-arg>
to chain listener
calls rather than breaking up the sequence into a series of artificial
private events.
The events caused by user action are only part of the picture in a Mach-II application. These are public events generated by the user’s interaction with the application. The event handlers for these events should be declared public by using the access="public" attribute of the event-handler command. All other event handlers, meaning those that handle events generated by the application itself as opposed to events initiated by the users, should be declared access="private" so they cannot be announced via the URL by the user. The event handler for the default event, which is the event that is invoked if the user does not explicitly specify an event in the URL, should be declared access="public". Even though by default events without an access declaration will be made public, it is good practice to explicitly declare the access for each event for the sake of clarity and readability.
Declaring event handlers as private ensures they cannot be invoked via the URL. For example, after a successful login attempt, a private event called loginSucceeded might be announced. It would not be wise to make this event public because it could be announced by any user via the URL. Granted, if someone had not logged in the application likely wouldn’t recognize them even if they did announce loginSucceeded via the URL, but it is simply good practice to make all events that do not need to be announced via the URL private. Another benefit is that if certain events cannot be announced via the URL, data validation is less of a concern because the application itself will be the only data provider for these events. As an aside, event filters may be used for data validation as well as manipulating event data. See the “Designing Event Filters For Mach-II” section below for further details.
In most OO programming languages it is generally recommended to declare all public entities first with private entities last. That is also good practice to follow for event-handler declarations, with each group in alphabetical order so that event handlers are easier to find within long mach-ii.xml files.
If a listener needs to announce an event, choose an event name that suits the listener rather than the application as a whole. In other words, use application-neutral events. The event-mapping tag can be used to map the listener's events to those recognized by the application. This is once again driven by the concepts of cohesion and coupling to improve reuse and maintainability. The use of application-neutral events helps decouple the listener from the application that uses it and may allow for the listener to be reused in other applications.
For example, a login listener should announce loginSucceeded or loginFailed (or more specific failures such as loginBadPassword, loginNoSuchUser, etc.), and the event handler should then map these events to specific events that are appropriate to the application:
<event-handler event="adminLogin">
<event-mapping event="loginSucceeded" mapping="showAdmin" />
<event-mapping event="loginFailed" mapping="showLogin" />
<notify listener="adminListener" method="login" />
</event-handler>
In this example, the login()
method of adminListener would announce the
application-neutral event loginSucceeded or loginFailed through a call
to announceEvent()
within the listener code, but the event mappings
would cause this event announcement to be translated so that what is
placed in the event queue is showAdmin
or showLogin
respectively.
The event mappings are active only from the <event-mapping>
command to
the end of the event handler, therefore the <event-mapping>
command
must precede the listener notifications that it is intended to
influence.
It is quite common to see similar code across several event handlers as an application is developed. For example, Edit and Create operations tend to have a similar flow beyond the initial capturing and loading of data, which can be seen in the Contact Manager sample application. Building complex layouts is another operation that tends to create common code at the end of several event handlers. In each of these cases, consider moving the common operations into a separate event handler and simply announcing a new common event from each of the original event handlers.
Even if the code isn't identical, the variables can often be abstracted
and turned into common code. In this situation, the original event
handlers will perform a certain amount of setup. For example,
<event-arg>
can be used to define values that differ between the two
code paths, and then a new common event could be announced.
In both cases, the common event should be declared access="private" following the guidelines above, because these common events would never need to be announced via the URL. The intent of this discussion is to reduce code duplication and look for code reuse.
If an event handler is separated into multiple private event handlers, some thought must be given as to how to data will be passed between the independent events. For example, assume the following event handler:
<event-handler event="processForm" access="public">
<announce event="handleFormData" />
<announce event="displayResult" />
</event-handler>
Since handleFormData and displayResult are both announced from the processForm event handler, they are both new event objects which both have a copy of the event arguments from the processForm event object. However, any changes made to the event object inside the event handler for handleFormData will not be reflected in the event object that is passed to the displayResult event handler, because handleFormData and displayResult are distinct events. This means that if communication between these two event handlers is desired it must be addressed in some way since this communication does not occur automatically.
In this case the ability to chain the events together could be useful. In other words, chaining the events together would take the implicit sequence from the code above and turn it into a more explicit sequence. The sequence above is implicit because it relies on the event queue to process handleFormData first and then displayResult second. Remember, however, that the event queue does not guarantee an execution order, and if handleFormData itself announces another event this may introduce additional complexity to the situation.
A more explicit solution would be to have the event handler for handleFormData explicitly announce the next event in the sequence and pass the event arguments to the next event so that any changes to the event object made in the first event are passed through to the second event. If handleFormData is only announced from one event handler, you can do this:
<event-handler event="processForm" access="public">
<announce event="handleFormData" />
</event-handler>
<event-handler event="handleFormData" access="private">
... handle the form data ...
<announce event="displayResult" />
</event-handler>
In real-world applications, it is likely that handleFormData is
announced from more than one event handler and that displayResult is not
always the next event to announce. Because of this fact, the next event
to be announced must be dynamically selected. There are several ways to
accomplish this, but the cleanest solution is probably to use
<event-arg>
to store the subsequent event name and then use an event
filter to announce the next event. This technique is sometimes called
continuation:
<event-handler event="processForm" access="public">
<event-arg name="continuationEvent" value="displayResult" />
<announce event="handleFormData" />
</event-handler>
<event-handler event="handleFormData" access="private">
... handle the form data ...
<filter name="continuation" />
</event-handler>
The continuation filter simply announces the event specified by continuationEvent:
<cfset arguments.eventContext.announceEvent(
arguments.event.getArg("continuationEvent"),
arguments.event.getArgs()
) />
When an exception occurs in a filter or listener method, the framework
catches the exception, creates a MachII.util.Exception object containing
the details of the exception, and then announces the exception event
that is defined in the <properties>
section in the mach-ii.xml file.
The handleException()
Plugin Point can be used to provide custom
processing when an exception occurs. An <event-mapping>
can also be
used to cause a specific event to be triggered when an exception occurs.
For example:
<event-handler event="someEvent" access="public">
<event-mapping event="defaultException" mapping="someFilterException" />
<filter name="someFilter" />
<event-mapping event="defaultException" mapping="someListenerException" />
<notify listener="someListener" method="someMethod" />
</event-handler>
In this example, it is assumed that defaultException is the event
defined by the exceptionEvent property in the <properties>
section of
the mach-ii.xml file. If an exception occurs in someFilter, the event
someFilterException will be announced by the framework. If an exception
occurs in someListener.someMethod()
, the event someListenerException
will be announced by the framework. If no event mapping is active, the
defaultException event will be announced when an exception occurs. An
event mapping is active from the point it is declared to the end of that
event handler.
This section discusses designing and using event filters. The first issue to consider is, given the desired functionality, if the correct object to use is an event filter or a plugin. After addressing that question briefly, this section examines some practical reasons for using event filters that may help improve the structure and flow of your application, and then examines larger issues such as form handling, validation, and security.
When first learning Mach-II it can be difficult to determine if you need an event filter or a plugin to achieve the desired functionality. By asking a few questions about the desired functionality it is relatively simple to determine which type of object will be more appropriate to a given situation:
- Does an operation need to be performed at the start or end of every single request?
- Does an operation need to be performed at the start or end of every single event handler?
- Does an operation need to be performed at the start or end of rendering every single view?
If the answer to any of these questions is "yes," the appropriate object to use is a plugin. Plugins will be discussed in a subsequent section.
If the above questions do not apply to a particular situation, consider the following questions:
- Does an operation need to be performed on the data provided to specific events?
- Does an operation need to be performed on the data returned by specific listener methods?
- Do certain event handlers need to be aborted conditionally?
If the answer to any of these questions is "yes," the appropriate object to use is an event filter.
The simplest way to think about event filters and plugins is that plugins are called on every event, while event filters are called only on the specific events in which they are declared. Plugins also provide a large number of plugin points allowing for operations to be performed at various discrete points during an event, while event filters are less granular in nature. In short, if some operation needs to be performed on every event, use a plugin, and if an operation only needs to be performed on specific events, use an event filter.
If the answer to all of the questions above is "no" but extending the application through the use of an event filter or plugin is desired, the rest of this section provides additional detail that can assist in the decision-making process.
An event filter is a component that provides a filterevent()
method that
is called by Mach-II, and this method is passed three arguments:
- The current event object
- The current eventContext object
- Optionally, a struct of name/value pairs from the
<parameter>
tags included in the filter invocation in mach-ii.xml
The filterevent()
method can either return true, in which case
processing of the event handler continues after the filterevent()
method
completes, or it can return false, in which case processing of the
current event handler is terminated. If the event is terminated,
processing continues with the next event in the queue unless the event
queue is cleared by calling arguments.eventContext.clearEventQueue()
in
the filter. Usually when a filter returns false it will also announce a
new event before doing so. If the filter clears the event queue then it
must also announce a new event in order for the application to continue.
An event filter can define a configure()
method if it needs to perform
initialization. As with listeners, the configure()
method of event
filters is called automatically by the framework when the application is
initialized. Event filters, like all other parts of the Mach-II
framework, are stored in application scope so their instance variables
are effectively application scope variables.
A minimal event filter CFC looks like this:
<cfcomponent extends="MachII.framework.EventFilter">
<cffunction name="configure" returntype="void" access="public" output="false">
<!--- perform any initialization --->
</cffunction>
<cffunction name="filterEvent" returntype="boolean" access="public" output="false">
<cfargument name="event" type="MachII.framework.Event" required="yes" />
<cfargument name="eventContext" type="MachII.framework.EventContext" required="yes" />
<cfargument name="paramArgs" type="struct" required="yes" />
<!--- return false if you need to abort the current event --->
<cfreturn true /> <!--- indicates success --->
</cffunction>
</cfcomponent>
The filter is declared in the <event-filters>
section of the
mach-ii.xml file as follows:
<event-filter name="filterName" type="Path.To.YourFilter" />
In this example, filterName is the name you want to give the filter within the mach-ii.xml file and Path.To.YourFilter
is the fully qualified component name for YourFilter
.cfc. You may optionally specify parameters for the filter declaration:
<event-filter name="filterName" type="Path.To.YourFilter">
<parameters>
<parameter name="param1" value="value1" />
<parameter name="param2" value="value2" />
</parameters>
</event-filter>
This provides default parameter values for param1 and param2 (of value1
and value2 respectively). These can be accessed within the filter using
the getParameter()
method (see below).
Remember that filters must be called in each event handler in which they are to be used; this is in contrast to plugins, which are automatically called on every event and therefore do not need to be declared within each event handler. The syntax for calling a filter in an event handler is as follows:
<filter name="filterName" />
This causes the filterevent()
method to be called, and Mach-II passes
the current event, the current event context, and an empty paramArgs
struct to the filterevent()
method.
You may optionally specify parameters to pass to the filter:
<filter name="filterName">
<parameter name="param1" value="newValue1" />
<parameter name="param3" value="value3" />
</filter>
This causes the filterevent()
method to be called, and in this case
Mach-II passes the filterevent()
method the current event, the current
event context, and a paramArgs struct containing two keys: param1 with a
value of newValue1, and param3 with a value of value3. The intent is
that parameter values passed in this way override any default parameter
value specified in the declaration of the filter. This situation is
typically managed with code similar to this:
<cffunction name="filterEvent" returntype="boolean" access="public" output="false">
<cfargument name="event" type="MachII.framework.Event" required="yes" />
<cfargument name="eventContext" type="MachII.framework.EventContext" required="yes" />
<cfargument name="paramArgs" type="struct" required="yes" />
<cfset var param1 = getParameter("param1") />
<cfset var param2 = getParameter("param2") />
<cfset var param3 = getParameter("param3") />
<!--- other var declarations --->
<cfif structKeyExists(paramArgs,"param1")>
<cfset param1 = paramArgs.param1 />
</cfif>
<cfif structKeyExists(paramArgs,"param2")>
<cfset param2 = paramArgs.param2 />
</cfif>
<cfif structKeyExists(paramArgs,"param3")>
<cfset param3 = paramArgs.param3 />
</cfif>
<!--- perform filter processing --->
<!--- return false if you need to abort the current event --->
<cfreturn true /> <!--- indicates success --->
</cffunction>
An event filter has access to both the current event and the current event context, so it is able to affect the logical flow of an application by manipulating the event context. For example, when an event filter aborts the current event by returning false, it generally needs to announce another event for the framework to execute, and it may also decide to clear any events that are waiting in the event queue. By convention, event filters that may abort processing and clear the event queue typically have a parameter that specifies the next event to announce, as well as a parameter that specifies whether or not to clear the event queue:
<cffunction name="filterEvent" returntype="boolean" access="public" output="false">
<cfargument name="event" type="MachII.framework.Event" required="yes" />
<cfargument name="eventContext" type="MachII.framework.EventContext" required="yes" />
<cfargument name="paramArgs" type="struct" required="yes" />
<cfset var invalidEvent = getParameter("invalidEvent") />
<cfset var clearEventQueue = getParameter("clearEventQueue") />
<!--- other var declarations --->
<cfif structKeyExists(paramArgs,"invalidEvent")>
<cfset invalidEvent = paramArgs.invalidEvent />
</cfif>
<cfif structKeyExists(paramArgs,"clearEventQueue")>
<cfset clearEventQueue = paramArgs.clearEventQueue />
</cfif>
<cfif someCondition>
<!--- note: clearEventQueue parameter is really a string --->
<cfif clearEventQueue is "true">
<cfset arguments.eventContext.clearEventQueue() />
</cfif>
<!--- pass current event's arguments into the new event: --->
<cfset arguments.eventContext.announceEvent(invalidEvent,arguments.event.getArgs()) />
<cfreturn false />
</cfif>
<cfreturn true />
</cffunction>
When user data enters an application, either as URL scope or form scope variables, performing some sort of validation on this data is generally required. This validation may be as simple as checking that certain variables have been provided or it may be something substantially more complex.
The Mach-II framework provides the RequiredFieldsFilter
event filter
that can be used to check if a specified list of URL or form scope
variables are present, and if one or more of the required fields is
missing, this filter announces a specified event and returns false to
abort the current event. This filter takes two parameters:
- requiredFields: a comma-separated list of field names that are to be checked
- invalidEvent: the event to announce if any fields are missing
If any fields are missing, the event that is announced has the same arguments as the current event plus the following additional arguments:
- message: an error message (in English)
- missingFields: a comma-separated list of field names that are missing
For more complex web form handling, a bean should be created to
encapsulate the web form data, and the <event-bean>
command can be
used to create the bean object from the event arguments. In addition,
the bean may provide a validate()
method that returns a boolean.
The ValidateFormObject
filter takes two required and one optional parameter:
- formObjectName: the name of the event argument on which to invoke validate()
- invalidEvent: the event to announce if validate() returns false
- clearEventQueue: an optional boolean that indicates whether or not to clear the event queue before announcing invalidEvent
If validate()
returns false, the filter also returns false after optionally clearing the event queue and then announcing the specified event with the same arguments as the current event, and it also adds the an additional argument called formObjectName, which is the name passed into the filter.
It is up to the specified event handler to determine how to deal with reporting the validation failure, which is likely to involve additional calls to the data object and, therefore, additional event filter invocations to manage that interaction.
Default parameters for the ValidateFormObject
event filter may be specified as follows:
<event-filter name="barValidator" type="Path.To.ValidateFormObject">
<parameters>
<parameter name="formObjectName" value="bar" />
<parameter name="invalidEvent" value="formHasInvalidBar" />
</parameters>
</event-filter>
<event-filter name="fooValidator" type="Path.To.ValidateFormObject">
<parameters>
<parameter name="formObjectName" value="foo" />
<parameter name="invalidEvent" value="formHasInvalidFoo" />
<parameter name="clearEventQueue" value="true" />
</parameters>
</event-filter>
These event filters can then be used without needing to specify the parameters each time, or the default values may be overridden:
<event-handler event="someEvent">
<event-bean name="bar" type="my.model.bar" />
<!--- this filter uses the default parameters --->
<filter name="barValidator" />
</event-handler>
<event-handler event="someOtherEvent">
<event-bean name="foo" type="my.model.foo" />
<!--- this filter overrides the default parameters --->
<filter name="fooValidator">
<parameter name="invalidEvent" value="warnAboutBadFoo" />
<parameter name="clearEventQueue" value="false" />
</filter>
</event-handler>
When user authorization needs to be performed on certain events, an event filter is probably the simplest way to achieve this. The Mach-II framework provides the PermissionsFilter event filter to support simple permission-based authorization checking. This filter checks that the current user has all of the necessary permissions (specified as a comma-separated list) and if they do not, it announces the event specified after optionally clearing the event queue. See the comments in the source code for more detail.
Extending the PermissionsFilter
component to implement a different security model would be relatively
trivial, and would involve overriding the getUserPermissions()
method to
return a comma-separated list of user permissions. If the default
inclusive permission checking behavior is not desired, or the user
permissions for a particular application are more complex than a
comma-separated list, the validatePermissions()
could also be overridden
to implement a different type of security check.
If the security needs of an application go beyond this rather simple permission type, a new security event filter would need to be written.
This section discusses the design and use of plugins in Mach-II applications. For information about determining if an event filter or a plugin will better suit a particular need, see the discussion in the section concerning event filters above.
A plugin is a component that provides methods that are called with the current eventContext (which is an instance of the MachII.framework.EventContext
object), and offers access to perform actions at various points during the request lifecycle as follows:
- preProcess(): called at the start of each request. At this point the
eventContext has the current event in the event queue but
getCurrentEvent()
will not yet return that event; see “Processing The First Event In A Request” below for additional details. - preEvent(): called for each event immediately prior to execution of
the relevant
<event-handler>
tag. At this point the eventContext contains the current event, i.e., the event that is about to be handled. - preView(): called for each view immediately prior to execution of
the relevant
<view-page>
tag. At this point the eventContext contains the current event, i.e., the event that is currently being executed. - postView(): called for each view immediately after execution of the
relevant
<view-page>
tag. At this point the eventContext contains the current event, i.e., the event that is currently being executed. - postEvent(): called for each event immediately after execution of
the relevant
<event-handler>
tag. At this point the eventContext contains the current event, i.e., the event that has just been handled and concluded. - postProcess(): called at the end of each request. At this point the eventContext no longer contains an event.
-
handleException()
: called whenever an exception is thrown back to the framework. When an exception occurs, the eventContext contains the current event if there is one. This plugin method is also passed an exception object of type MachII.util.Exception.
A plugin can define a configure()
method if it needs to perform
initialization. As with listeners and filters, the configure()
method is
called automatically by the framework when the application is
initialized. Plugins, like all other parts of the Mach-II framework, are
stored in application scope so their instance variables are effectively
application scope variables.
A minimal plugin CFC might look like this:
<cfcomponent extends="MachII.framework.Plugin">
<cffunction name="configure" returntype="void" access="public" output="false">
<!--- perform any initialization --->
</cffunction>
<cffunction name="preEvent" returntype="void" access="public" output="false">
<cfargument name="eventContext" type="MachII.framework.EventContext" required="yes" />
<!--- perform processing prior to every event being handled --->
</cffunction>
</cfcomponent>
This plugin overrides only the preEvent()
method, but it could override any of the seven methods contained in the base MachII.framework.Plugin object (see the methods listed above).
Plugins are declared in the <plugins>
section of the mach-ii.xml file as follows:
<plugin name="pluginName" type="Path.To.YourPlugin" />
As with event filters, parameters may optionally be specified in the plugin declaration:
<plugin name="pluginName" type="Path.To.YourPlugin">
<parameters>
<parameter name="param1" value="value1" />
<parameter name="param2" value="value2" />
</parameters>
</plugin>
This provides default parameter values for param1 and param2 (of value1
and value2 respectively). These can be accessed within the plugin using
the getParameter()
method, e.g., getParameter("param1")
.
The framework itself can handle all exceptions by using the exception
event and a single event handler. For many applications this will be
perfectly acceptable. Custom exception handling may also be added to an
application by implementing the handleException()
method in a plugin.
This plugin point is executed after an exception is encountered, but
before the exception event is announced and handled by the framework
itself or any other components that may handle the exception. After
executing this plugin point, the event queue is cleared and the
specified exceptionEvent (from the <properties>
section of
mach-ii.xml) is announced and handled.
Exceptions should not be thrown from handleException()
, and abortEvent()
should not be called, because both these operations will lead to an
unhandled exception that will be displayed to the end user. Furthermore,
a new event cannot be announced since the event queue is cleared after
handleException()
has been executed. However, information may be added
to the current event object, and this information can then be retrieved
in the exception event handler as follows:
<cfif arguments.eventContext.hasCurrentEvent()>
<cfset arguments.eventContext.getCurrentEvent().setArg("argName",argValue) />
</cfif>
This will set the event argument argName to the value argValue if an event was defined when the exception was encountered.
Inside the exception event handler, the current event is the exception event itself, which contains the following arguments:
- exception: the MachII.util.Exception object containing details of the original exception
- exceptionEvent: the MachII.framework.Event object that was being handled when the exception was thrown. This is only present if there was a current event defined when the exception occurred.
An <event-mapping>
may also be used to provide more fine-grained
control over Exception Handling in event handlers.
Since a current event is not set by the time the preProcess()
method of
a plugin is invoked, it is might not be obvious how to process just the
first event in each request:
<cffunction name="preProcess" returntype="void">
<cfargument name="eventContext" type="MachII.framework.EventContext" />
<!--- peek at the first event in the queue: --->
<cfset var firstEvent = arguments.eventContext.getNextEvent() />
<!--- ...process firstEvent here... --->
</cffunction>