Release 1.2.1 (4/14/2005)
« Designing Models | Contents | Designing Event Handlers »
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").« Designing Models | Contents | Designing Event Handlers »
RSS feed | Send me an e-mail when comments are added to this page | Comment Report
Current page: http://livedocs.adobe.com/wtg/public/machiidevguide/views.html
Comments
lanfeustdetroy said on Nov 14, 2003 at 5:58 AM : SeanCorfield said on Nov 25, 2003 at 12:22 PM : No screen name said on Dec 1, 2003 at 11:58 AM : SeanCorfield said on Dec 1, 2003 at 12:06 PM : schipperheyn said on Feb 22, 2004 at 5:54 AM :