Release 1.2.1 (4/14/2005)
This Development Guide is structured as follows:
This document explains how to develop applications using Mach II (Release 1.0.10), an MVC-based framework that implements the Implicit Invocation Architecture. You can read more about Mach II on the official Mach II website and Sean Corfield's unofficial Mach II website as well as Phil Cruz's Mach II information website. It is a living document, growing over time, incorporating feedback from Macromedia Web Team developers as well as the larger ColdFusion community, as the guidelines are 'proved' in our live environment.
Although this document is published as-is for the ColdFusion community, Macromedia's Web Team own the document and decide what goes into it. You may, however, take a copy of this document and modify it as you see fit to create your own development guide as long as you acknowledge this original document.
The following conventions are used in this document:
This section explains the concepts in the framework and describes how to access the core files. Much of this information is taken from my Mach II Concepts page.
The first concept is MVC - Model-View-Controller - where the presentation
(View) is completely separated from your business logic (Model) and interactions
between the two are handled by Mach II itself (Controller). The Views are simply .cfm files
that render HTML - presentation code - and can make use of request scope
variables and an event object (set by Mach II - more below). The Model is a set of ColdFusion Components
(CFCs) that represent all the logic of your application, from the database interaction
to the business rules and decisions. The Controller is, essentially, Mach II
itself and the parameters of the Controller are defined by the mach-ii.xml file.
Mach II has a slight grey area between the Model and the Controller. The framework has the concept of "listeners" which are CFCs that extend the framework and provide the glue between the core Controller and the core Model. These listener CFCs can be considered part of the Model or part of the Controller depending on how you like to structure your applications but best practice suggests you should keep your Model independent of the framework (so that it is easier to reuse for Web Services, Flash Remoting or even within another framework). The section on Designing Models has more information about this.
The second concept is the Implicit Invocation Architecture on which Mach II is based. You can read more about this on the Mach II website (Ben Edwards has a great paper on this). The basic premise is that events are generated (by the user or by the application) and they are handled by, well, event handlers. The event handlers respond to the events by performing business logic and sometimes generating more events. Mach II keeps track of the active events and processes them, invoking listeners as necessary and rendering views until each each request is fully processed.
These guidelines mention several design patterns (e.g., Session Façade, Memento, Transfer Object). Here are some good references on design patterns:
The framework is a collection of ColdFusion Components under the MachII directory.
That directory can be installed under your ColdFusion document root or on a
Custom Tag Path defined in the ColdFusion Administrator. At Macromedia, the MachII directory
is under CVS at:
{cfmxroot}/extensions/components/MachII/
Your application index.cfm file (under {cfmxroot}/wwwroot/{appname}/) simply
includes the mach-ii.cfm core file (using a mapping to the MachII directory,
if Mach II is installed outside the web root):
<cfinclude template="/MachII/mach-ii.cfm" />
Prior to including the mach-ii.cfm file, you can set your own values for MACHII_CONFIG_MODE,
MACHII_CONFIG_PATH and, if needed, MACHII_APP_KEY. By default, the mach-ii.cfm core
file uses the directory in which the request originated ({appname} in
our example) as a key into application scope to store that application's
instance of the framework objects. That can be overridden by setting MACHII_APP_KEY in index.cfm (but
is not generally recommended).
The mach-ii.cfm core file by default looks for ./config/mach-ii.xml (relative
to the {appname} directory), which it uses to initialize the framework
for that application. That path can be overridden by setting MACHII_CONFIG_PATH in index.cfm.
For security purposes, it is recommended that you place your mach-ii.xml file
outside your document root so that it cannot be accessed through a web browser. At
Macromedia, we override MACHII_CONFIG_PATH as
follows:
<cfset MACHII_CONFIG_PATH = expandPath("/environment/{appname}/mach-ii.xml") />
Remember that /environment is a mapping to {cfmxroot}/config/target/ -
a directory created by our build system - so the actual configuration file,
under CVS, is {cfmxroot}/config/development/{appname}/mach-ii.xml and
can be overridden for staging, integration and production if necessary although
this is not recommended (we have better mechanisms for handling environment-specific
variables).
Note: For CFMX 6.1, expandPath() with a mapping works on Unix
(Solaris and Mac OS X) but does not work on Windows - expandPath() does
not process the mapping. This has been fixed in CFMX 7.
MACHII_CONFIG_MODE is set to 0 (dynamic) by default
in the core files (since release 1.0.8). This causes Mach II to dynamically reload
the framework only if mach-ii.xml changes. This is good for development
but it can also be useful to add the following code to index.cfm:
<cfif not request.productionMode and
structKeyExists(URL,"reloadApp")>
<cfset MACHII_CONFIG_MODE = 1 />
</cfif>
This allows you to force a reload easily during development by adding ?reloadApp to
the URL. In production, you'd normally set MACHII_CONFIG_MODE to -1 (never).
Here's a quick summary of the impact of each of the three modes:
1) - Pro: changes to listeners are picked up immediately. Con: slow
(because the framework is reloaded each time), can hide subtle bugs (because
if your listener stores data in variables scope it will behave like data in request scope - since the
listener is recreated for each request).0) - Pro: listener instance data behaves correctly (persisting from
request to request), easy to force a reload by changing the mach-ii.xml configuration
file. Con: changes to listeners are not automatically picked up (you have
to change the XML file to pick up listener changes). -1) - Pro: creates a stable production environment (you can run builds
to production servers without changing the application behavior, so you can
control when the changes are activated - by restarting the server). Con:
you have to restart the server to pick up any changes.At
Macromedia, we set a default for MACHII_CONFIG_MODE in sitewideconstants.cfm to dynamic for development and QA environments and to never for production environments. The standard mach-ii.cfm file uses this default (request.MachIIConfigModel) if it is defined and you have not already overridden MACHII_CONFIG_MODE in your index.cfm file.
This section explains how to structure your application and provides some general design guidelines for building object-oriented applications.
This section explains how to structure your application to fit both Mach II and our standard directory structure guidelines.
This has already been discussed under Mach II Core Files. This is the central configuration file for a Mach II application that specifies all the listeners, events and views etc. As mentioned above, it lives outside the document root.
This is a directory structure convention used by Mach II to separate the CFCs
used by an application. Although it is not required to follow the Mach II convention,
it is actually fairly convenient. Under your application components directory,
create these three directories: model, filters, plugins. At Macromedia, all
our components live outside the document root so we end up with this structure:
{cfmxroot}/extensions/components/{appname}/model/
{cfmxroot}/extensions/components/{appname}/filters/
{cfmxroot}/extensions/components/{appname}/plugins/
CFCs in the model directory are a combination of listeners that extend MachII.framework.Listener and framework-neutral business model components.
CFCs in the filters directory are event filters that extend MachII.framework.EventFilter.
CFCs in the plugins directory are plugins that extend MachII.framework.Plugin.
Other CFCs that are used to support the application can live in either the
model directory or if appropriate in another more suitable directory,
e.g., when wrapping existing application CFCs in a Mach II application skin.
To keep the model directory organized, it may be a good idea to create separate sub-directories within it for the listeners if you have several of them, or even create a listeners directory directly under your {appname} directory:
.../components/{appname}/model/listeners/ - just the listeners
.../components/{appname}/model/ - other, framework-neutral, business objects
or:
.../components/{appname}/listeners/ - just the listeners
.../components/{appname}/model/ - other, framework-neutral, business objects
Another possibility is to keep your listeners and business objects together but clearly identify your listeners through a naming convention, e.g., SomethingListener.cfc.
Which approach you choose will likely depend on how many listeners your application has and whether or not you are reusing the business objects in another context.
Another directory structure convention, this is used to separate out the .cfm files that render HTML. These .cfm files live in the views directory underneath
the application directory, either under the document root or under the include
directory for that application. At Macromedia, we put our views outside the
document root:
{cfmxroot}/extensions/includes/{appname}/views/
Since we have a mapping from /cfinclude to {cfmxroot}/extensions/includes/ we
define the global Mach II property applicationRoot, in mach-ii.xml,
to be "/cfinclude/{appname}" instead
of just "/{appname}" so that Mach II can find the view .cfm files.
This section provides guidance on designing your Mach II application. We start with some general design guidelines in this section and then subsequent sections cover models - including design patterns - then views, event handlers, event filters and finally plugins.
You can approach the design of a Mach II application from two different view points: from the business model or from the user experience. In most cases, you will be able to perform a certain amount of the design in parallel in these two areas. As you will see in the sections below, the business model components can be designed and implemented independently of the rest of the application (and usually independently of the Mach II framework itself). The user interface can also be designed and - for the most part - implemented independently of the rest of the application. The interactions in the user interface drive the design of the event model in a Mach II application which then, in turn, drives the design of the event handlers and listeners, constrained by the interface of the business model. For teams where the user interface work and back end 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.
For many developers, especially those new to Object-Oriented Design, the hardest part of building an application in an OO style is determining which components are needed. If you can write a short coherent narrative that describes the interactions in your business logic, you'll find that most of the nouns in that narrative suggest themselves as components and most of the verbs suggest themselves as methods on those components. Most - not all. 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 might be important enough to be modeled as a component that understands title, greeting, first (given) name, family name, middle name or initial, suffix etc but in another application it may suffice to have a string for full name (or perhaps two strings for first and last name). There are no hard and fast rules.
Once you have your components identified, you need to consider how well-defined they are. Each component should be highly cohesive - it should do (or represent) one "thing" only and do it well. The components should also be loosely coupled - they should not need to know about or depend on each other in order to do their "one thing". Be prepared to refactor your code to improve the cohesion and reduce the coupling - your first choice of components may be obvious but won't necessarily be the best representation of your business logic. If you find it hard to represent a relationship between two or more components, perhaps the relationship itself is sufficiently important to need its own component - sometimes the relationship may actually be more important than the entities it connects. Also remember that a relationship between two components represents coupling between those components. Abstracting the relationship into its own component can often reduce the coupling between other components and so provide more flexibility.
The other key point to consider is inheritance versus composition. Many OO novices see inheritance everywhere and try to have components extend other components left, right and center. Inheritance is a very special relationship between components: it means "is-a". It really isn't as common as most people seem to think and it is also a form of very tight coupling between the parent and child components. 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. Composition can be broadly described as "has-a" or "refers-to" relationships. A good example of this is the Employee / Manager model. Novices often try to model this with inheritance - a Manager "is-a" Employee - but this breaks down when you start to consider promotions and substitutions and so on. 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 other people's work by learning about and using Design Patterns - they are your friend!
This section looks at design issues in the Model portion of your application. The Model encompasses all of the business logic in your application and is implemented as a collection of ColdFusion Components.
Mach II interacts with the Model portion of your application using components
that extend MachII.framework.Listener. In a simple application,
all of your business logic might be implemented in such listeners but this approach
does not scale well with increasing application complexity because, amongst other
things, it introduces a tight coupling between the elements of your business
logic and the Mach II framework. If you've carefully designed your business components
based on the guidelines in the previous section, it should be clear that only
a few of those components are natural "listeners" for events. It might
even be that none of your basic business components are natural "listeners".
This is not a problem!
You are much better off ensuring there is only a thin layer of coupling between the framework and your application - if necessary, create new components that extend the Mach II listener component, whose sole purpose is to communicate those events to the components within your business model. In other words, try to isolate your business components from the framework components as much as possible: only listener components should know about the 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 have no dependencies at all on the framework.
A minimal listener CFC that uses the CFCInvoker_Event invoker
type looks like this:
<cfcomponent extends="MachII.framework.Listener"> <cffunction name="configure" returntype="void" access="public" output="false"> <!--- perform any initialization ---> </cffunction> <cffunction name="someMethod" returntype="someType" access="public" output="false"> <cfargument name="event" type="MachII.framework.Event" required="yes" /> <!--- perform some task ---> <cfreturn someValue /> </cffunction> </cfcomponent>
The method, someMethod, can be invoked from an event
handler in mach-ii.xml using
the <notify> command. The returned value, someValue,
should be of type someType. Note that someType can
be void if the method does not return anything - either it does
not contain a <cfreturn /> tag or it specifies no expression
in <cfreturn />.
The listener is declared in mach-ii.xml as follows:
<listener name="listenerName" type="Path.To.YourListener"> <invoker type="MachII.framework.invokers.CFCInvoker_Event" /> </listener>
The <invoker> tag specifies how the methods should be invoked
on the listener. In general, you should specify CFCInvoker_Event as
shown above which will cause the current event object to be passed as a single
argument to the invoked method. You can also specify CFCInvoker_EventArgs which
will cause the current event's arguments to be passed individually as named
arguments into the method. See the section on Invokers
& Listeners that follows
for more detail.
You may optionally specify parameters for the listener declaration:
<listener name="listenerName" type="Path.To.YourListener"> <invoker type="MachII.framework.invokers.CFCInvoker_Event" /> <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 Listener base class).
The methods of a listener CFC can be invoked in an event handler as follows:
<notify listener="listenerName" method="someMethod" resultKey="someVariable" />
This causes the someMethod() method to be called with
the current event and the returned result to be stored in someVariable,
e.g., request.result. The resultKey attribute is
optional (and should not be specified for methods that have returntype="void").
A listener has access only to the current event although it can announce additional
events to be added to the queue (and which will be executed at some point after the current event has been handled) using announceEvent().
A listener method typically does its job by calling one or more methods on
one or more business model objects. The business model objects might be created
on the fly, might be managed in session scope or might be created
within the listener's configure() (and stored in the listener's variables scope).
For more information on writing listeners, refer to the Mach II article How to... Develop Listeners by Ben Edwards.
When a listener is declared in mach-ii.xml, you have to specify
an invoker type. Mach II provides two default types:
CFCInvoker_EventArgsCFCInvoker_EventThe invoker affects how arguments are passed to the listener methods in a <notify> tag and what happens to the result.
As noted above, CFCInvoker_EventArgs causes
all the event arguments to be passed to the method as separate named arguments. CFCInvoker_Event causes
the whole event object to be passed as a single argument (of type MachII.framework.Event)
to the method. Both invokers store the result in the specified variable, which means you have to use a request scope variable. To some extent, which you use is a matter of style but there
are some practical considerations:
CFCInvoker_EventArgs may seem to provide better type safety
since you can specify the type of each argument (and whether it is required
and, if not, whether it should have a default) but letting ColdFusion itself
validate what is really your URL and form data is not really very robust!CFCInvoker_Event provides more flexibility at the expense
of requiring additional code inside your listener method to access and validate
the event arguments.CFCInvoker_Event and consider using event
filters to enhance the type safety of your application. If you use CFCInvoker_Event then your listener methods are passed a single argument which should be declared as follows:
<cfargument name="event" type="MachII.framework.Event" required="true"/>
You can test whether a given URL / form variable was provided using the isArgDefined() method on the event:
<cfif arguments.event.isArgDefined("anArg")>
...
</cfif>
Since URL / form variables are inherently strings (as far as HTTP is concerned), you are better off explicitly validating the event argument types in your own code, rather than trying to use CFCInvoker_EventArgs and declaring method arguments for individual URL / form variables (you don't really want ColdFusion to throw an exception if a user mistypes something in an input field!).
You can also create custom invokers if you need a different behavior such as this invoker to put the result directly into the event object, setting the event argument specified by the resultKey= attribute:
<cfcomponent extends="MachII.framework.ListenerInvoker" output="false" hint="I am a custom invoker" displayName="EventInvoker"> <cffunction name="invokeListener" access="public" returntype="void" output="false" hint="I implement the method invocation for a listener"> <cfargument name="event" type="MachII.framework.Event" required="true" hint="I am the current event" /> <cfargument name="listener" type="MachII.framework.Listener" required="true" hint="I am the listener that is being notified" /> <cfargument name="method" type="string" required="true" hint="I am the method that is being invoked" /> <cfargument name="resultKey" type="string" default="" hint="I name the optional event argument in which the result is stored" /> <cfset var resultVar = 0 /> <cftry> <cfinvoke component="#arguments.listener#" method="#arguments.method#" event="#arguments.event#" returnVariable="resultVar" /> <cfif arguments.resultKey is not ""> <cfset arguments.event.setArg(arguments.resultKey,resultVar) /> </cfif> <cfcatch type="Any"> <cfrethrow /> </cfcatch> </cftry> </cffunction> </cfcomponent>
Since Mach II loads all the framework component instances into application scope,
you need to bear in mind that any instance data you create in your listener
will effectively be stored in application scope. That has three main implications:
variables scope. <cflock type="exclusive" name="..."> .. </cflock> around
any updates on variables scope data, with an appropriately
chosen lock name.In general, your listeners should be stateless - with no instance data - unless
you are specifically caching data for performance reasons, e.g., saving property
values in variables scope to save accessing the properties dynamically
in each request. Consequently, you should take extra care to use var to
declare all local variables in listener methods so that you don't accidentally store
something in the unnamed scope! Remember that tags like <cfquery> create variables too so you must var-declare those as well:
<cfset var userSelect = 0 /> <cfquery name="userSelect" ..> ... </cfquery>
As mentioned above, if you need to manage per-session data in your application,
you will need to use the Session Façade design pattern. The way this
works is that only your listener is session-aware, i.e., it knows about session scope
and manages component instances that live in session scope but
those per-session component instances are not listeners themselves (they do
not extend MachII.framework.Listener)
and they do not reference session scope. For example, a shopping
cart listener would respond to events like 'addItem' and 'removeItem' and 'updateQuantity'
but it would delegate the actions to a cart object that is stored in session scope.
The shopping cart listener would create the cart object in 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 session scope.
<cfcomponent 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 for your application).
The basic event lifecycle in Mach II is:
resultKey)If you need more than one piece of data in your view, you could notify the
listener multiple times, creating several resultKeys and having
the view depend on several request scope variables. It seems like
the obvious approach so what is the impact of doing this?
request scope variables are required) These should raise red flags - especially the last two, which cause encapsulation
to break down and coupling to increase. When are you likely to see this pattern
occurring in your 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 etc. The obvious - naïve - approach would be to have
getter methods for each of these on the listener (with calls for each of these
in the event handler) and have the view depend on request.firstName, request.lastName,
request.streetAddress etc. Ugly.
The Transfer Object design pattern is intended for situations like this and
solves the problem by aggregating data that is passed between the model and
the view. In the above example, you would have a single getPerson() method
which returns a struct (that contains all of the data needed) and the view
would then have a single dependency on that one resultKey. If
you want a little more encapsulation on your transfer object, you could use
a bean - a simple CFC that has getters and setters for the data and a constructor
(init())
- which the listener component uses to set all the data in the transfer object
before returning it to Mach II.
As indicated above, a bean is a simple CFC with getters and setters that encapsulates
some data (properties). 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 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 only in that the setters are public.
The constructor has an optional argument for each property and calls setXxx() for
each property xxx.
Mach II supports bean creation and population through the <event-bean> command:
<event-bean name="beanName" type="beanType" fields="field1,field2" />
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).
fields= is specified, the command calls
the constructor - init() - with no arguments and then calls
the setter for each field specified in the list
of fields (e.g., setField1(event.getArg("field1")), setField2(event.getArg("field2"))). fields= is omitted, the command calls the constructor with
all the current event's arguments by name (e.g., init(field1=event.getArg("field1"),
field2=event.getArg("field2"))).This makes it very easy to handle form submissions in Mach II: define a
bean component to represent the data in a form and use the <event-bean> command
to assemble it from the form data submitted. Then you can operate on
the submitted data as an encapsulated bean, performing validation (see Designing
Event Filters) and persistence and whatever else you need.
For more information on writing and using beans, refer to the Mach II article Beans, Beans, the Musical Fruit by Ben Edwards and Hal Helms.
Whilst this topic is not directly related to the Mach II framework, most applications need to implement data access (usually to a database) and many people ask for guidance on this subject.
There are two basic patterns of access to persistent data within most applications:
ColdFusion has a great built-in idiom for dealing with the first type of access
- the query - which is an efficient way to manipulate (potentially
large) sets of data rows retrieved from a database (or other data sources,
since you can easily create a query object and populate it with
your own data). When you are dealing with aggregated access, it does not make
sense to convert every row returned into a fully-encapsulated object (CFC instance)
when all you are likely to do with the data is display a few fields with a
link to a detail page that will focus on the selected row.
On the other hand, when you are focusing on a single row it usually does make sense to work at the fully-encapsulated object level since you are usually interested in object behavior at that point. This is also the level where you need the standard CRUD (Create, Read, Update, Delete) operations.
Recognizing these two basic patterns, you should design your components accordingly by providing separate components for each pattern. This is best explained through an example:
Order, then we would
provide an OrderGateway component for aggregate access and an OrderDAO component
for per-object access (or build the per-object access into the Order object
- but see below).OrderGateway component would provide methods like findAll(),
findWhere(), findByID() and they would all return standard query objects
(even findByID() which returns a single row).OrderDAO component would provide CRUD methods like store(), load(),
update(), delete() and they would operate on a
specific Order object exchanging
data via getters/setters on the Order component or via some
sort of snapshot of the Order's data, e.g., a bean: the Order component
could implement methods like getSnapshot() returning a bean and setSnapshot() taking a bean as an argument - the bean containing the core persistent data for the Order object.
Sometimes a more direct data transfer between the business object and the
data access object is needed for performance reasons (or because the application
can 'trust' the components to exchange less encapsulated data, such as a
struct or some opaque data structure e.g., the Memento design pattern).
Such optimizations are beyond the scope of this document.Separating out these operations from the business model object helps it stay persistence-neutral. The gateway components can be optimized for retrieving large record sets, caching etc. The DAO components can be optimized for dirty data updates, pooled object access and so on. Again, the details of these optimizations are beyond the scope of this document but providing for the two distinct patterns of data access will get you started on the right road.
As noted above, you can implement the CRUD methods directly in your business model object if you wish, although these guidelines recommend using the separate DAO component outlined above. The pros of implementing CRUD in your business model object are:
The cons are:
As you can imagine, it is the driving forces of good encapsulation, high cohesion and loose coupling that lead to the recommendation above to provide gateway and DAO components that are separate from the business model components.
To see more concrete examples of these concepts, visit mach-ii.info and download the sample application which uses a number of these design patterns.
This section looks at design issues in the View portion of your application.
The View encompasses all of the HTML user inteface in your application and
is implemented as a collection of .cfm pages.
Since views should not contain any logic (beyond that necessary to iterate
over data structures to display them as lists), any dynamic portions
of a view should be passed in from the controller (Mach II), either as request scope
variables or via the event object (accessible in a view as just plain event).
The section on Transfer
Objects touches on this topic
and recommends that views depend on as few variables
as possible to maintain encapsulation and reduce coupling.
The key thing to remember is that the request scope variables and the arguments in the event object that are used by a view define the API between the controller
and the view and thus specifies a contract that should be agreed early on in
the design, documented and then honored for rest of the project lifecycle if
possible. This helps maintainability and stability.
You can reduce that API down to just the current event object by moving the request scope data into the event object:
<event-arg name="foo" variable="request.foo" />
Whether you do this or not depends on whether you feel the increased number
of <event-arg> commands is worth the increase in encapsulation.
An alternative is to use the custom invoker, EventInvoker, shown in the Invokers & Listeners section of Designing Models. Our rule of thumb is to leave data where it naturally falls rather than move
it around artificially just to increase encapsulation. In other words, we use
request scope in views for data that already happens to be in request scope
(e.g., as set up by contentKey=) and
we use the event object for data that already happens to be in the event object
(e.g., URL or form parameters, values explicitly
set with <event-arg> or event.setArg() inside filters, listeners and plugins or via EventInvoker notify commands).
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 (typically their
submit and cancel actions are passed in as well as the submit button label).
This is much better than hard-coding the submit and cancel actions into the
view itself.
Fusebox uses a similar technique and refers to it as XFAs - eXit FuseActions - so by analogy, these could be described as XEs - eXit Events. Again, it reduces coupling between views and the application control flow so it's good practice to parameterize views where appropriate so that the exit paths out of a view (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 in the mach-ii.xml file.
Mach II allows views to be rendered into contentKey variables
so that a single HTML page can be constructed from multiple views (page fragments).
These contentKey variables are typically in request scope
so they also form part of the API between the controller and the views, as
discussed above. Care therefore needs to be taken in planning these too. As
a general guideline, you will produce a more maintainable application if you
can arrange it so that dynamic data (request scope variables and / or the event object)
is passed into simple views that are rendered into contentKey variables
and then those contentKey variables are aggregated into a single
layout template view. For example:
<event-handler event="showFoo" access="public"> <notify listener="foo" method="getFoo" resultKey="request.fooData" /> <view-page name="fooPage" contentKey="request.content" /> <view-page name="mainLayout" /> </event-handler>
In this example, the API between fooPage and the controller is
simply request.fooData (which might be a struct containing multiple
items) and the API between mainLayout and the controller is simply request.content.
There is no dependency between mainLayout and any dynamic data
- all dynamic data is rendered into HTML by other views prior to rendering
mainLayout.
If you have several views that go together to make up part of a layout you can sometimes simplify your event handler by appending to a contentKey (this is new in 1.0.10):
<event-handler event="showFoo" access="public"> <notify listener="foo" method="getFoo" resultKey="request.fooData" /> <view-page name="fooPod" contentKey="request.content" /> <view-page name="barPod" contentKey="request.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 request.content so you do not need additional contentKeys and layouts.
The intent here is for each view to focus on rendering just the data that it depends on and using a layout view to assemble the finished page from pre-rendered HTML fragments. This keeps each view cohesive: each view does one job well. It 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 where you are in the flow of the application. Examine your user interface with an eye to breaking it 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 (often called pods).
Implement each of these pods as a separate view that renders a request scope variable or some event arguments into
a contentKey and use a layout view to assemble those contentKey variables
into the finished HTML page. This will produce small, focused (cohesive) views
that are loosely coupled. You are also more likely to be able to reuse user
interface elements on other pages - because you can reuse views very easily
in Mach II as long as they have only narrow, well-defined dependencies. Even
if these pods contain only static content, it is still usually better to implement
them as views so that you can rearrange page elements more easily - because
you only have to change the layout view. This is sometimes referred to as "view
stacking" but is really an implementation of the Composite
View design pattern
(see J2EE
pattern description).
This raises the question of whether headers and footers should be implemented
as separate pod views and assembled by the layout view? If the header and footer
are site-wide elements, the chances are that they will only change if the whole
look and feel of the site changes. Implementing them as separate views is possible
but then every single event-handler that generates HTML will look
like this:
<event-handler event="..." access="..."> ... <view-page name="..." contentKey="request.content" /> <view-page name="header" contentKey="request.header" /> <view-page name="footer" contentKey="request.footer" /> <view-page name="mainLayout" /> </event-handler>
Or, to reduce code duplication in mach-ii.xml, you could do this:
<event-handler event="..." access="..."> ... <view-page name="..." contentKey="request.content" /> <announce event="layoutPage" /> </event-handler> <event-handler event="layoutPage" access="private"> <view-page name="header" contentKey="request.header" /> <view-page name="footer" contentKey="request.footer" /> <view-page name="mainLayout" /> </event-handler>
Every event-handler that needs to generate HTML would announce
the layoutPage event. If you are comfortable with this style,
feel free to implement headers and footers as separate views. If you feel that
headers and footers are so closely tied to the overall page layout then either
implement them directly inside the layout view or implement them as separate
files and include them into the layout view:
<!--- doctype etc goes here ---> <html> <!--- head / title etc go here ---> <body> <cfinclude template="header.cfm" /> #request.content# <cfinclude template="footer.cfm" /> </body> </html>
At Macromedia, we use custom tags to control the overall rendering of pages so our layout view looks somewhat simpler:
<!--- set up page settings etc ---> <cfimport taglib="/customtags/mmlf/" prefix="mmlf" /> <mmlf:renderpage> #request.content# </mmlf:renderpage>
If you've followed the recommendations above, your view will contain references
to a few request scope variables and / or the current
event object (event). That is effectively the interface between
the controller and that view. What about using other scopes inside views?
application and server scope: no.application scope
and therefore can manage
application scope data (as instance data) which is then explicitly
passed out via request scope or the event object (resultKey) 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
request scope or the event object to views).CGI scope: no.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
you need to select different layouts based on, say, the user's browser, you
can manage this by using a listener to announce different events based on
the CGI variables and handling those events by rendering
different layouts.
cookie scope: no.cookie scope inside a view should be avoided (and viewed with
some suspicion!). 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.session scope and a view
needs access to that information, then it is acceptable to directly refer
to session scope within a view. Strictly speaking, encapsulation
would oblige us to use a listener or a filter to copy the necessary data
into request scope or the event object for the view but for applications that
rely on session data extensively, this is likely to be too burdensome in
terms of code complexity so it is not worth it in this case (not only would
you have to copy all the necessary
session scope data into request scope or the event object, expanding
the view API, but if you perform any operations 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
you have only limited reliance on session scope, you can probably use a filter
to copy session variables to request variables or the event object with little overhead. Note
that we do not use client scope
at Macromedia so that is not covered in these recommendations. URL and form scope: no.URL or form scope directly.
Form data should be handled through a bean that aggregates the form
into a single event argument (using the <event-bean> command
or a filter like the ContactBeanerFilter in
the ContactManager sample application). Views can retrieve URL and form scope
values from the event object using event.getArg("argName").This section looks at design issues in the event-handlers section
of mach-ii.xml. If you've followed the Overall
Design Considerations, you will have designed (and probably
implemented) both your core business model components and your HTML views by
the time you started designing your event handlers. In terms of designing the
views, simple interaction schematics should be a sufficient level of detail
to enable you to start work on designing the event handlers.
Every user interaction corresponds to an event: every link and every form
submission is an event, including the default event implied by the first page
a user requests. Since the public event names will be a visible (user-facing) part
of your application, it's worth choosing readable event names of the form verbNoun.
Use mixed case for event names - 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 are used to Fusebox, which uses circuit.fuseaction).
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 out into the event model, thus you will find a large number of events are implied by your application logic. For example, whenever you handle an event that has conditional flow, such as login which can succeed or fail, you should usually create event handlers for each of the conditional outcomes - see Application Neutral Events below. Even when the flow is linear, it is often worth separating out 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.verb.noun.
On the other hand, don't break event handlers up unnecessarily - if you have
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. You can use <event-arg> to move request scope data into the
event object like this:
<notify listener="myListener" method="methodOne" resultKey="request.result" /> <event-arg name="oneArg" variable="request.result" /> <notify listener="myListener" method="methodTwo" resultKey="request.result" />
The events caused by user interactions are only part of the picture in a Mach
II application - they are the public events, generated by external stimuli:
users. The event handlers for these events should be declared access="public".
All other event handlers - those that handle events generated by the application
itself - should be declared access="private". The event
handler for the default event - the one that is invoked when the user does
not explicitly specify an event in the URL - should be declared access="public".
Declaring event handlers private ensures they cannot be invoked via URL hackery. That means that you only need to worry about user input data validation in public event handlers and then set up event data before handing off control to internal (private) event handlers. Use event filters for validation as well as manipulating event data - see the section on Designing Event Filters For Mach II.
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 your listener needs to announce events, choose event names that suit the
listener rather than the application as a whole, i.e., use application-neutral
events. You can use the event-mapping tag 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:
using application-neutral events helps decouple your 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 then the event handler should map those to whatever the application needs:
<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 -
using announceEvent() - but the event mappings would cause this
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 (and 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 you are developing your application. For example, Edit and Create operations tend to have a similar flow beyond the initial capture / load of data (as you can see in the ContactManager 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, you can often abstract out the variables
in it and turn it into common code. In that situation, your original event
handlers will perform a certain amount of setup, e.g., using event-arg to
define values that differ between the two code paths, and then announce the
new common event.
In both cases, the common event should be declared access="private",
following the guidelines above. The intent here is to reduce code duplication
and to look for reuse.
If you separate an event handler into multiple private event handlers, you need to think about how to communicate data between them:
<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 - they are distinct events. That means that communication between those
two event handlers must be done through request scope variables
which is less than ideal.
What is really needed here is the ability to "chain" the events together -
in other words to make the sequence that is implicit in the above code into
an explicit sequence. Why is the sequence implicit above? Because it relies
on the event queue - to process handleFormData first and then displayResult second.
A more explicit solution would be to have the event handler for handleFormData explicitly
announce the next event in the sequence and pass on the event arguments so
that any changes to the event object are passed on. 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 that, you need to dynamically
select the next event to announce. There are several ways to do this but the
cleanest solution is probably to use <event-arg> to store
the next event name and then an event filter to announce that - 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, as defined
in the
<properties> section in the mach-ii.xml file.
You can use The handleException() Plugin
Point to provide some custom processing
when an exception occurs. You can also use <event-mapping> to cause a specific
event to be triggered when an exception occurs:
<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 looks at designing and using event-filters. The
first issue to consider is whether you want an event filter or a plugin. Having
decided that you want an event filter, this section looks at some practical
reasons for using them to improve the structure and flow of your application
and then examines larger issues such as form handling, validation and security.
When you first start using Mach II, it can be difficult to tell whether you need an event filter or a plugin. You need to ask yourself a few questions about what you are trying to achieve so you can determine which will work better for the problem you are trying to solve:
If you answered "yes" to any of those questions, you probably need a plugin.
If you answered "yes" to any of those questions, you probably need an event filter.
If you answered "no" to all of the above but still think you need an event filter or a plugin, then read through the rest of this section and the next section, on plugins, and decide for yourself.
An event filter is a component that provides a filterEvent() method
which is called with three arguments:
eventeventContext<parameter> tags
in the filter invocationfilterEvent can either return true, in which case
processing of the event handler continues, or it can return false,
in which case processing of the current event handler is terminated (and processing
continues with the next event in the queue - unless you clear the event queue
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 it
clears the event queue, it must announce a new event after doing so!).
An event filter can define a configure() method if it needs to
perform initialization - this is called automatically by the framework. 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 /> <!--- indicate success ---> </cffunction> </cfcomponent>
The filter is declared inside the <event-filters> .. </event-filters> tags in mach-ii.xml as follows:
<event-filter name="filterName" type="Path.To.YourFilter" />
where 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.
The filter is used in an event handler as follows:
<filter name="filterName" />
This causes the filterEvent() method to be called with the current event,
the current event context and an empty paramArgs struct.
You may optionally specify parameters for the filter usage:
<filter name="filterName"> <parameter name="param1" value="newValue1" /> <parameter name="param3" value="value3" /> </filter>
This causes the filterEvent() method to be called with 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 would override any default parameter value specified in
the declaration of the filter. You would typically manage this with code
that looked like 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 /> <!--- indicate 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 decides to abort 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 queued up for execution. By convention, event filters
that do that usually have a parameter that specifies the 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 your application, either as URL scope or form scope
variables, you generally need to perform some sort of data validation. This
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
to allow you to check that a specified list of URL or form scope
variables are all present, announcing a specified event if one or more is
missing and returning false to abort the current event. This
filter takes two parameters:
requiredFields - a comma-separated list of field names that are to be checkedinvalidEvent - the event to announce if any fields are missingIf 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, you should create a bean to encapsulate
the web form data (and use the <event-bean> command to create
the bean object from the event arguments) and provide a validate() method
on that data object that returns a boolean. Then you can use the ValidateFormObject filter
(from corfield.org) to invoke that method and handle any failure. This filter
takes two or three parameters:
formObjectName - the name of the event argument on which to invoke validate()invalidEvent - the event to announce if validate() returns false clearEventQueue - optional, boolean, indicates whether to
clear the event queue before announcing invalidEventIf 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 plus the following additional
argument:
formObjectName - the name passed into the filterIt is up to the specified event handler 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.
You can specify default parameters
for the ValidateFormObject event filter:
<event-filter name="barValidator" type="MachII.filters.ValidateFormObject"> <parameters> <parameter name="formObjectName" value="bar" /> <parameter name="invalidEvent" value="formHasInvalidBar" /> </parameters> </event-filter> <event-filter name="fooValidator" type="MachII.filters.ValidateFormObject"> <parameters> <parameter name="formObjectName" value="foo" /> <parameter name="invalidEvent" value="formHasInvalidFoo" /> <parameter name="clearEventQueue" value="true" /> </parameters> </event-filter>
You can now use these event filters without needing to specify the parameters each time or you can override the defaults:
<event-bean name="bar" type="my.model.bar" /> <filter name="barValidator" /> ... <event-bean name="foo" type="my.model.foo" /> <filter name="fooValidator"> <parameter name="invalidEvent" value="warnAboutBadFoo" /> <parameter name="clearEventQueue" value="false" /> </filter>
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.
You may be able to simply extend the PermissionsFilter component
to implement your own security, overriding the getUserPermissions() method
to return a comma-separated list of user permissions. If you don't want the
default inclusive permission checking behavior, or your user permissions are
more complex than a comma-separated list, you can also override validatePermissions() and
implement your own checking.
If your security system is more complex than simple permissions, you will need to write your own security event filter.
This section looks at designing and using plugins.
A plugin is a component that provides methods which are called
with the current eventContext (an object of type MachII.framework.EventContext)
at various points in the lifecycle of a request:
preProcess() - called at the start of each request: 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).preEvent() - called for each event, immediately prior to execution of the
relevant event-handler tag: the eventContext contains the current event (the
one that is about to be handled).preView() - called for each view, immediately prior to execution of the
relevant view-page tag: the eventContext contains
the current event (the one that is being executed).postView() - called for each view, immediately after execution of the
relevant view-page tag: the eventContext contains the current event (the
one that is being executed).postEvent() - called for each event, immediately after execution of
the relevant event-handler tag: the eventContext contains the current event (the one that has just been handled).postProcess() - called at the end of each request: the eventContext
no longer contains an event.handleException() - called whenever an exception is thrown
back to the framework: the eventContext contains the current event (if
there is one). This plugin method is also passed the exception object (of
type MachII.util.Exception). A plugin can define a configure() method if it needs to
perform initialization - this is called automatically by the framework. 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 just the preEvent() method but it could
override any of the seven of the methods listed above.
The plugin is declared inside the <plugins> .. </plugins> tags in mach-ii.xml as follows:
<plugin name="pluginName" type="Path.To.YourPlugin" />
You may optionally specify parameters for 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").
handleException() Plugin Point You can let the framework handle exceptions by using the exception event
and using a single event handler. For many applications this will be perfectly
acceptable. You can also add custom exception handling to an application by
implementing the handleException() method on a plugin. This plugin
point is executed after an exception is encountered, before the exception event
is announced and handled. 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.
You
should not throw an exception from handleException() nor call abortEvent() -
both operations will lead to an unhandled exception that will be displayed
to the end user. You cannot announce a new event (since the event queue
is cleared after
handleException() has been executed). You can add information
to the current event object which can then be retrieved in the exception event
handler:
<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 has the following arguments:
exception - the MachII.util.Exception object containing details of the original
exceptionexceptionEvent - 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) You can use <event-mapping> to provide finer-grained control
over Exception Handling in event handlers.
Since the "current event" is not set by the time preProcess() 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>
Note: prior to 1.0.9, the above call to getNextEvent() would
remove the first
event from the queue!
Date |
Author |
Description |
|---|---|---|
3/7/2005 |
Sean A Corfield |
Release 1.2.1: Contents: no change; Concepts: fixed typos, clarified |
3/10/2004 |
Sean A Corfield |
Release 1.2: Contents: updated to 1.0.10 core files, added mach-ii.info as a reference site; Concepts: added note that listeners straddle the line between Controller and Model, clarified that Mach II itself recognizes |
2/4/2004 |
Sean A Corfield |
Release 1.1.1: Contents: updated to 1.0.9 core files;
Concepts: no change; Structure: no
change; Models:
fixed typo in |
1/22/2004 |
Sean A Corfield |
Release 1.1: Contents: added reference links for
design patterns; Concepts:
updated to 1.0.8 core files; added note about |
10/27/2003 |
Sean A Corfield |
Release 1.0.3: Contents: reflect 1.0.7 core files; Concepts:
updated to reflect 1.0.7 core files (common |
10/9/2003 |
Sean A Corfield |
Release 1.0.2: Corrected some typos (S Page); Expanded Data Access Objects section to better explain DAO, gateway and memento machinery - and added example code (various requesters); Fixed wording in explanation of Implicit Invocation Architecture (Leo Schuman); Made forward / back references into hyperlinks (various requesters); Revisited recommendations about views accessing the current event; Split document into multiple sections (WTG); Fixed several internal links (WTG); Corrected event filter examples (WTG). |
10/8/2003 |
Sean A Corfield |
Release 1.0.1: Corrected wording in event filter and plugin sections
regarding |
10/8/2003 |
Sean A Corfield |
Release 1.0: Initial public release. |
Send me an e-mail when comments are added to this page | Comment Report
Current page: http://livedocs.adobe.com/wtg/public/machiidevguide/printable.html