-
Notifications
You must be signed in to change notification settings - Fork 24
Primer Part 7: Processing Data with Beans and DAOs Using Event Filters
- Create a Company
- Creating the showCompanyForm event
- Filtering data
- Procedural version
- Mach-II version
- What is a Mach-II Event Filter?
- Other Chapters
We've seen how to convert data list and data detail pages from procedural code to Mach-II events. Now we need to refactor how we build form and action pages.
From a data list page, we generally have the options to Add a New Item, View Item Details or Edit Item.
Clicking on "Add a Company", we get a new Company form.
Here's the procedural version of this page:
/mach-ii-primer/classic/company_form.cfm
<html>
<head>
<title>Classic CF Demo - Company Form</title>
</head>
<body>
<cfinclude template="includes/header.cfm">
<cfparam name="url.COMPANY_ID" type="numeric" default="0" />
<table width="770" cellspacing="0" cellpadding="4" align="center" border="0">
<tr>
<td valign="top" bgcolor="Silver" width="100">
<cfinclude template="includes/lhs_navigation_menu.cfm">
</td>
<td valign="top">
<h2>Company Form</h2>
<cfquery name="qCompanies" datasource="#request.DSN#">
SELECT
COMPANY_ID,
COMPANY_NAME,
COMPANY_ADDRESS_ONE,
COMPANY_ADDRESS_TWO,
COMPANY_CITY,
COMPANY_STATE,
COMPANY_ZIP,
COMPANY_PHONE_MAIN
FROM
CF_COMPANIES
WHERE
COMPANY_ID = '#url.COMPANY_ID#'
</cfquery>
<script type="text/javascript">
function validateCompany()
{
var f = document.companyData;
var err = "";
if (f.COMPANY_NAME.value == "")
{
err += "Please enter a Company Name.\n";
}
if (f.COMPANY_ADDRESS_ONE.value == "")
{
err += "Please enter an Address.\n";
}
if (f.COMPANY_CITY.value == "")
{
err += "Please enter the City.\n";
}
if (f.COMPANY_STATE.value == "")
{
err += "Please enter the State.\n";
}
if (f.COMPANY_ZIP.value == "")
{
err += "Please enter the Zip Code.\n";
}
if (f.COMPANY_PHONE_MAIN.value == "")
{
err += "Please enter the Main Phone Number.\n";
}
if (err != "")
{
alert(err);
return false;
}
return true;
}
</script>
<cfoutput>
<form name="companyData"
action="company_form_process.cfm"
method="post"
onSubmit="return validateCompany()">
<input type="hidden" name="COMPANY_ID" value="#qCompanies.COMPANY_ID#" />
<p>Company Name: <input type="text" name="COMPANY_NAME" value="#qCompanies.COMPANY_NAME#" /></p>
<p>Address Line 1: <input type="text" name="COMPANY_ADDRESS_ONE" value="#qCompanies.COMPANY_ADDRESS_ONE#" /></p>
<p>Address Line 2: <input type="text" name="COMPANY_ADDRESS_TWO" value="#qCompanies.COMPANY_ADDRESS_TWO#" /></p>
<p>City: <input type="text" name="COMPANY_CITY" value="#qCompanies.COMPANY_CITY#" /></p>
<p>State: <input type="text" name="COMPANY_STATE" value="#qCompanies.COMPANY_STATE#" /></p>
<p>Zip: <input type="text" name="COMPANY_ZIP" value="#qCompanies.COMPANY_ZIP#" /></p>
<p>Main Phone: <input type="text" name="COMPANY_PHONE_MAIN" value="#qCompanies.COMPANY_PHONE_MAIN#" /></p>
<p>
<cfif url.COMPANY_ID eq 0>
<input type="submit" name="addCompany" value="Add Company" />
<cfelse>
<input type="submit" name="editCompany" value="Edit Company" />
<input type="submit" name="deleteCompany" value="Delete Company" />
</cfif>
<input type="button" name="cancel" value="Cancel" onClick="history.back();" />
</p>
</form>
</cfoutput>
</td>
</tr>
</table>
<cfinclude template="includes/footer.cfm">
</body>
</html>
The link to "Add a new Company" passes COMPANY_ID=0 in the querystring, so the query qCompanies returns an emtpy record set, so the form fields are all populated with empty strings.
We've already refactored the layout HTML into Mach-II <page-view>s. All that's left are three major sections of code:
- The query qCompanies.
- Javascript to validate the form's contents.
- The form itself.
First we need a query that returns a single company record. We already
have this code in the CompanyListener
, which uses the Company bean and
CompanyDAO.
mach-ii-primer/m2/model/companies/05/CompanyListener.cfc
<notify listener="CompanyListener" method="getCompanyDetail" resultArg="companyBean" />
<cffunction name="getCompanyDetail"
access="public"
output="false"
returntype="Company"
hint="returns a populated Company Bean.">
<cfargument name="event" type="MachII.framework.Event" required="true" />
<cfset var company = createObject("component", "Company").init( arguments.event.getArg("COMPANY_ID") ) />
<cfset var companyDAO = createObject("component", "CompanyDAO").init( DSN = request.DSN ) />
<cfset companyDAO.read(company) />
<cfreturn company />
</cffunction>
Next we need to move the Company form and Javascript to its own CFM file.
<page-view name="companyForm" page="/views/companies/05/company_form.cfm" />
The values of the form fields are now being populated by the contents of the Company bean.
mach-ii-primer/views/companies/05/company_form.cfm
<cfset company = event.getArg("companyBean") />
<cfoutput>
<form name="companyData" action="index.cfm?event=processCompanyData" method="post" onSubmit="return validateCompany()">
<input type="hidden" name="COMPANY_ID" value="#company.getCompanyID()#" />
<p>Company Name: <input type="text" name="COMPANY_NAME" value="#company.getName()#" /></p>
<p>Address Line 1: <input type="text" name="COMPANY_ADDRESS_ONE" value="#company.getAddressOne()#" /></p>
<p>Address Line 2: <input type="text" name="COMPANY_ADDRESS_TWO" value="#company.getAddressTwo()#" /></p>
<p>City: <input type="text" name="COMPANY_CITY" value="#company.getCity()#" /></p>
<p>State: <input type="text" name="COMPANY_STATE" value="#company.getState()#" /></p>
<p>Zip: <input type="text" name="COMPANY_ZIP" value="#company.getZip()#" /></p>
<p>Main Phone: <input type="text" name="COMPANY_PHONE_MAIN" value="#company.getPhoneMain()#" /></p>
<p>
<cfif company.getCompanyID() eq 0>
<input type="submit" name="addCompany" value="Add Company" />
<cfelse>
<input type="submit" name="editCompany" value="Edit Company" />
<input type="submit" name="deleteCompany" value="Delete Company" />
</cfif>
<input type="button" name="cancel" value="Cancel" onClick="history.back();" />
</p>
</form>
</cfoutput>
Optionally, we can move the Javascript to an external file.
mach-ii-primer/js/05/companies.js
function validateCompany()
{
var f = document.companyData;
var err = "";
if (f.COMPANY_NAME.value == "")
{
err += "Please enter a Company Name.\n";
}
if (f.COMPANY_ADDRESS_ONE.value == "")
{
err += "Please enter an Address.\n";
}
if (f.COMPANY_CITY.value == "")
{
err += "Please enter the City.\n";
}
if (f.COMPANY_STATE.value == "")
{
err += "Please enter the State.\n";
}
if (f.COMPANY_ZIP.value == "")
{
err += "Please enter the Zip Code.\n";
}
if (f.COMPANY_PHONE_MAIN.value == "")
{
err += "Please enter the Main Phone Number.\n";
}
if (err != "")
{
alert(err);
return false;
}
return true;
}
If we use an external JS file, then we need to reference it in company_form.cfm.
<cfif event.getArg("jsFile") is not "">
<script src="#event.getArg("jsFile")#" type="text/javascript"></script>
</cfif>
The event-handler for showCompanyForm
<!--
mach-ii.05.xml
-->
<event-handler event="showCompanyForm" access="public">
<event-arg name="pageTitle" value="Manage Company" />
<!-- Optional external JS file -->
<event-arg name="jsFile" value="/mach-ii-primer/m2/js/05/companies.js" />
<notify listener="CompanyListener" method="getCompanyDetail" resultArg="companyBean" />
<view-page name="header" />
<view-page name="lhsMenu" contentArg="sidebar" />
<view-page name="companyForm" contentArg="mainContent" />
<view-page name="template" />
<view-page name="footer" />
</event-handler>
The showCompanyForm
event will be the starting point for Creating,
Updating and Deleting Company records.
The form is setup to show different submit actions based on url.COMPANY_ID.
<cfif company.getCompanyID() eq 0>
<input type="submit" name="addCompany" value="Add Company" />
<cfelse>
<input type="submit" name="editCompany" value="Edit Company" />
<input type="submit" name="deleteCompany" value="Delete Company" />
</cfif>
When you have multiple submit buttons in a form, whichever one you click will be the name/value pair that exists on the action page. (This is basic HTML functionality.)
In the procedural version, the form submits to a processing page.
<form name="companyData"
action="company_form_process.cfm"
method="post"
onSubmit="return validateCompany()">
A different query is run based on which submit button was used to submit the form.
mach-ii-primer/classic/company_form_process.cfm
<cfif structKeyExists(form, "addCompany")>
<cfquery name="qAddCompany" datasource="#request.DSN#">
INSERT INTO
CF_COMPANIES
(
COMPANY_NAME,
COMPANY_ADDRESS_ONE,
COMPANY_ADDRESS_TWO,
COMPANY_CITY,
COMPANY_STATE,
COMPANY_ZIP,
COMPANY_PHONE_MAIN
)
VALUES
(
'#form.COMPANY_NAME#',
'#form.COMPANY_ADDRESS_ONE#',
'#form.COMPANY_ADDRESS_TWO#',
'#form.COMPANY_CITY#',
'#form.COMPANY_STATE#',
'#form.COMPANY_ZIP#',
'#form.COMPANY_PHONE_MAIN#'
)
</cfquery>
</cfif>
<cfif structKeyExists(form, "editCompany")>
<cfquery name="qEditCompany" datasource="#request.DSN#">
UPDATE
CF_COMPANIES
SET
COMPANY_NAME = '#form.COMPANY_NAME#',
COMPANY_ADDRESS_ONE = '#form.COMPANY_ADDRESS_ONE#',
COMPANY_ADDRESS_TWO = '#form.COMPANY_ADDRESS_TWO#',
COMPANY_CITY = '#form.COMPANY_CITY#',
COMPANY_STATE = '#form.COMPANY_STATE#',
COMPANY_ZIP = '#form.COMPANY_ZIP#',
COMPANY_PHONE_MAIN = '#form.COMPANY_PHONE_MAIN#'
WHERE
COMPANY_ID = #form.COMPANY_ID#
</cfquery>
</cfif>
<cfif structKeyExists(form, "deleteCompany")>
<cfquery name="qDeleteCompany" datasource="#request.DSN#">
DELETE
FROM
CF_COMPANIES
WHERE
COMPANY_ID = #form.COMPANY_ID#
</cfquery>
</cfif>
<cflocation URL="companies.cfm" />
Now we're going to submit the form to an event instead of a page.
<form name="companyData"
action="index.cfm?event=processCompanyData"
method="post"
onSubmit="return validateCompany()">
mach-ii.05.xml
<event-handler event="processCompanyData" access="public">
<filter name="FilterCompanyAction"/>
<announce event="home" />
</event-handler>
An event filter is an object (component/CFC) that allows you to run some
logic, then return true or false based on the results. Any filter that
you create extends MachII.framework.EventFilter
.
<cfcomponent displayname="FilterCompanyAction"
output="no"
extends="MachII.framework.EventFilter"
hint="Processes Company data based on the Submit button clicked.">
The required function filterEvent
takes 3 arguments.
filterEvent()
<cffunction name="filterEvent" access="public" output="false" returntype="boolean">
<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" />
</cffunction>
You can optionally create a configure()
method to handle anything that
needs to run when the Filter is created. This works the same as a
Listerner's configure()
method.
All Filters are created when the application loads for the first time. They are placed into the application scope, so be aware of your variable scopes.
When a filter returns true, then Mach-II continues processing the
event-handler. In the event-handler processCompanyData
, if the filter
FilterCompanyAction
returns true, then the announce command is run and
we proceed to the "home" event.
FilterCompanyAction
returns true
<filter name="FilterCompanyAction"/>
<announce event="home" />
When a filter returns false, Mach-II stops processing the rest of the
current event-handler and goes to the next one. Usually you'll announce
the next event before returning false. (mach-ii-primer/m2/filters/05/FilterCompanyAction.cfc
)
<cfcomponent displayname="FilterCompanyAction"
output="no"
extends="MachII.framework.EventFilter"
hint="Processes Company data based on the Submit button clicked.">
<cffunction name="filterEvent" access="public" output="false" returntype="boolean">
<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" />
<cfif event.getArg("addCompany") is not "">
<cfset announceEvent("processCompanyCreate", arguments.event.getArgs()) />
<cfreturn false />
</cfif>
<cfif event.getArg("editCompany") is not "">
<cfset announceEvent("processCompanyUpdate", arguments.event.getArgs()) />
<cfreturn false />
</cfif>
<cfif event.getArg("deleteCompany") is not "">
<cfset announceEvent("processCompanyDelete", arguments.event.getArgs()) />
<cfreturn false />
</cfif>
<cfreturn true />
</cffunction>
</cfcomponent>
In the above code, if the "addCompany" submit button was clicked, then
event.getArg("addCompany")
will not be an empty string, so the Filter
then announces the event processCompanyCreate
and copies all of the
current event Args to that event. Then it returns false, telling Mach-II
to stop processing the current event-handler (processCompanyData
).
So far, we have replaced part of the functionality of the procedural file "company_form_process.cfm". Now we just need to handle the database interactions.
So we start off at showCompanyForm
.
<event-handler event="showCompanyForm" access="public">
<event-arg name="pageTitle" value="Manage Company" />
<notify listener="CompanyListener" method="getCompanyDetail" resultArg="companyBean" />
<view-page name="header" />
<view-page name="lhsMenu" contentArg="sidebar" />
<view-page name="companyForm" contentArg="mainContent" />
<view-page name="template" />
<view-page name="footer" />
</event-handler>
The form in that event submits to the event processCompanyData
.
<event-handler event="processCompanyData" access="public">
<filter name="FilterCompanyAction"/>
<announce event="home" />
</event-handler>
Based on the action (button) we chose in showCompanyForm
, the filter
FilterCompanyAction
then announces the next event.
If we chose to CREATE a Company
<event-handler event="processCompanyCreate" access="private">
<notify listener="CompanyListener" method="createCompany" />
<announce event="showCompanies" />
</event-handler>
The event processCompanyCreate
notifies the Listener CompanyListener
to call the method createCompany
.
The method createCompany
creates an instance of the CompanyDAO
, then creates and populates an instance of the Company bean with the form data and finally passes the Company bean to the create()
method of the DAO. (mach-ii-primer/m2/model/companies/05/CompanyListener.cfc - New function: createCompany()
)
<cffunction name="createCompany"
access="public"
output="false"
returntype="boolean"
hint="Creates a new Company record.">
<cfargument name="event" type="MachII.framework.Event" required="true" />
<cfset var companyDAO = createObject("component", "CompanyDAO").init( DSN = request.DSN ) />
<cfset var company = createObject("component", "Company").init(
CompanyID = arguments.event.getArg("COMPANY_ID"),
Name = arguments.event.getArg("COMPANY_NAME"),
AddressOne = arguments.event.getArg("COMPANY_ADDRESS_ONE"),
AddressTwo = arguments.event.getArg("COMPANY_ADDRESS_TWO"),
City = arguments.event.getArg("COMPANY_CITY"),
State = arguments.event.getArg("COMPANY_STATE"),
Zip = arguments.event.getArg("COMPANY_ZIP"),
PhoneMain = arguments.event.getArg("COMPANY_PHONE_MAIN")
) />
<cfreturn companyDAO.create( company ) />
</cffunction>
mach-ii-primer/m2/model/companies/05/CompanyDAO.cfc - New function: create()
<cffunction name="create"
access="public"
returntype="boolean"
output="false"
hint="Create a new Company record.">
<cfargument name="company" type="Company" required="true" hint="Company bean" />
<cfset var qAddCompany = "" />
<cftransaction>
<cftry>
<cfquery name="qAddCompany" datasource="#variables.DSN#">
INSERT INTO
CF_COMPANIES
(
COMPANY_NAME,
COMPANY_ADDRESS_ONE,
COMPANY_ADDRESS_TWO,
COMPANY_CITY,
COMPANY_STATE,
COMPANY_ZIP,
COMPANY_PHONE_MAIN
)
VALUES
(
<cfqueryparam value="#arguments.company.getName()#" cfsqltype="cf_sql_varchar" />,
<cfqueryparam value="#arguments.company.getAddressOne()#" cfsqltype="cf_sql_varchar" />,
<cfqueryparam value="#arguments.company.getAddressTwo()#" cfsqltype="cf_sql_varchar" />,
<cfqueryparam value="#arguments.company.getCity()#" cfsqltype="cf_sql_varchar" />,
<cfqueryparam value="#arguments.company.getState()#" cfsqltype="cf_sql_varchar" />,
<cfqueryparam value="#arguments.company.getZip()#" cfsqltype="cf_sql_varchar" />,
<cfqueryparam value="#arguments.company.getPhoneMain()#" cfsqltype="cf_sql_varchar" />
)
</cfquery>
<cfcatch type="database">
<cftransaction action="rollback" />
<cfreturn false />
</cfcatch>
</cftry>
</cftransaction>
<cfreturn true />
</cffunction>
Once all of this has processed, the event showCompanies is loaded.
If we chose to UPDATE a Company
<event-handler event="processCompanyUpdate" access="private">
<notify listener="CompanyListener" method="updateCompany" />
<announce event="showCompanies" />
</event-handler>
The event processCompanyUpdate
notifies the Listener CompanyListener
to call the method updateCompany
.
The method updateCompany creates an instance of the CompanyDAO
, then
creates and populates an instance of the Company bean with the form data
and finally passes the Company bean to the update()
method of the DAO.
mach-ii-primer/m2/model/companies/05/CompanyListener.cfc - New function: updateCompany()
<cffunction name="updateCompany"
access="public"
output="false"
returntype="boolean"
hint="Updates a Company record.">
<cfargument name="event" type="MachII.framework.Event" required="true" />
<cfset var companyDAO = createObject("component", "CompanyDAO").init( DSN = request.DSN ) />
<cfset var company = createObject("component", "Company").init(
CompanyID = arguments.event.getArg("COMPANY_ID"),
Name = arguments.event.getArg("COMPANY_NAME"),
AddressOne = arguments.event.getArg("COMPANY_ADDRESS_ONE"),
AddressTwo = arguments.event.getArg("COMPANY_ADDRESS_TWO"),
City = arguments.event.getArg("COMPANY_CITY"),
State = arguments.event.getArg("COMPANY_STATE"),
Zip = arguments.event.getArg("COMPANY_ZIP"),
PhoneMain = arguments.event.getArg("COMPANY_PHONE_MAIN")
) />
<cfreturn companyDAO.update( company ) />
</cffunction>
mach-ii-primer/m2/model/companies/05/CompanyDAO.cfc - New function: update()
<cffunction name="update"
access="public"
returntype="boolean"
output="false"
hint="Updates a Company record.">
<cfargument name="company" type="Company" required="true" hint="Company bean" />
<cfset var qEditCompany = "" />
<cftransaction>
<cftry>
<cfquery name="qEditCompany" datasource="#variables.DSN#">
UPDATE
CF_COMPANIES
SET
COMPANY_NAME = <cfqueryparam value="#arguments.company.getName()#" cfsqltype="cf_sql_varchar" />,
COMPANY_ADDRESS_ONE = <cfqueryparam value="#arguments.company.getAddressOne()#" cfsqltype="cf_sql_varchar" />,
COMPANY_ADDRESS_TWO = <cfqueryparam value="#arguments.company.getAddressTwo()#" cfsqltype="cf_sql_varchar" />,
COMPANY_CITY = <cfqueryparam value="#arguments.company.getCity()#" cfsqltype="cf_sql_varchar" />,
COMPANY_STATE = <cfqueryparam value="#arguments.company.getState()#" cfsqltype="cf_sql_varchar" />,
COMPANY_ZIP = <cfqueryparam value="#arguments.company.getZip()#" cfsqltype="cf_sql_varchar" />,,
COMPANY_PHONE_MAIN = <cfqueryparam value="#arguments.company.getPhoneMain()#" cfsqltype="cf_sql_varchar" />
WHERE
COMPANY_ID = <cfqueryparam value="#arguments.company.getCompanyID()#" cfsqltype="cf_sql_integer" />
</cfquery>
<cfcatch type="database">
<cftransaction action="rollback" />
<cfreturn false />
</cfcatch>
</cftry>
</cftransaction>
<cfreturn true />
</cffunction>
Once all of this has processed, the event showCompanies is loaded.
If we chose to DELETE a Company
<event-handler event="processCompanyDelete" access="private">
<notify listener="CompanyListener" method="deleteCompany" />
<announce event="showCompanies" />
</event-handler>
The event processCompanyDelete
notifies the Listener CompanyListener
to call the method deleteCompany
.
The method deleteCompany
creates an instance of the CompanyDAO
, then
creates and populates an instance of the Company bean with the
COMPANY_ID only and finally passes the Company bean to the delete()
method of the DAO.
mach-ii-primer/m2/model/companies/05/CompanyListener.cfc - New function: deleteCompany()
<cffunction name="deleteCompany"
access="public"
output="false"
returntype="boolean"
hint="Updates a Company record.">
<cfargument name="event" type="MachII.framework.Event" required="true" />
<cfset var companyDAO = createObject("component", "CompanyDAO").init( DSN = request.DSN ) />
<cfset var company = createObject("component", "Company").init(
CompanyID = arguments.event.getArg("COMPANY_ID")
) />
<cfreturn companyDAO.delete( company ) />
</cffunction>
mach-ii-primer/m2/model/companies/05/CompanyDAO.cfc - New function: delete()
<cffunction name="delete"
access="public"
returntype="boolean"
output="false"
hint="Updates a Company record.">
<cfargument name="company" type="Company" required="true" hint="Company bean" />
<cfset var qDelCompany = "" />
<cftransaction>
<cftry>
<cfquery name="qDelCompany" datasource="#variables.DSN#">
DELETE
FROM
CF_COMPANIES
WHERE
COMPANY_ID = <cfqueryparam
value="#arguments.company.getCompanyID()#"
cfsqltype="cf_sql_integer"
/>
</cfquery>
<cfcatch type="database">
<cftransaction action="rollback" />
<cfreturn false />
</cfcatch>
</cftry>
</cftransaction>
<cfreturn true />
</cffunction>
Once all of this has processed, the event showCompanies is loaded.
Ack! Carpel Tunnel!
Yes, that's a heck of a lot of code for such a small application. If this was a real world app, I'm sure I'd just create it the procedural way. However, the concepts and techniques shown so far are the building blocks used in many high traffic and mission critical applications.
You can see these changes in action by using the version 5 files.
- Rename the version 4 mach-ii.xml file to mach-ii.04.xml
- Rename mach-ii.05.xml to mach-ii.xml
- Reload the application and click the Companies link in the left-hand navigation.
The Mach-II Primer has now caught up with the Object Oriented
ColdFusion
Primer. I'm working on a post for that primer which will
cover a Complex DAO (working with multiple tables or data sources).
After that, I'll get into the Service Object and Object Factory. Once
those are done, I'll come back to the Mach-II Primer to show how to
implement those objects in the sample application.
But before then: We've been converting this application to an MVC framework in an effort to reduce the amount of redundant code. You should have noticed by now that there are a lot of the same objects being created in the Listeners. They're being created every time a function is called. That's redundant code too.
- Mach-II Primer Series Navigation
- Part 1 - Files versus Events
- Part 2 - Variables versus Arguments
- Part 3 - Flip This Code
- Part 4 - Using Gateways to manage record sets
- Part 5 - Mediating events with Listeners
- Part 6 - Getting the details with Beans and DAOs
- Part 7 - Processing data with Beans and DAOs using Event-Filters
Special thanks to Adrian J. Moreno of IKnowKungFoo for contributing this series of primers.