LISTING 1: FlexForums.cfc

<cfcomponent displayname="FlexForum" output="false" hint="CFC for use with the Flex interface to
 Ray Camden's Galleon forum software">
    <!--- THREAD METHODS --->
    <cffunction name="getThreads" access="remote" returntype="query" output="false" hint="Returns
	 a list of threads.">
        <cfargument name="forumId" type="numeric" required="false" default="0" />

        <cfset var qGetThreads = "" />

        <cfquery name="qGetThreads" datasource="galleon">
            SELECT threads.id, threads.name, threads.readonly,
                threads.active, threads.forumidfk, threads.useridfk,
                threads.datecreated, forums.name as forum,
                users.username, max(messages.posted) as lastpost,
                count(messages.id) as messagecount
            FROM threads, forums, users, messages
            WHERE threads.forumidfk *= forums.id
            AND threads.useridfk *= users.id
            AND threads.active = 1
            <cfif arguments.forumid gt 0>
                AND threads.forumidfk = <cfqueryparam value="#arguments.forumId#"
				 cfsqltype="cf_sql_integer" />
            </cfif>
            AND messages.threadidfk =* threads.id
            GROUP BY threads.id, threads.name, threads.readonly, threads.active,
                threads.forumidfk, threads.useridfk, threads.datecreated, forums.name,
                users.username
            ORDER BY lastpost DESC
        </cfquery>

        <cfreturn qGetThreads />
    </cffunction>

    <cffunction name="addThread" access="remote" returntype="void" output="false" hint="Adds a
	 thread.">
        <cfargument name="threadName" type="string" required="true" />
        <cfargument name="threadBody" type="string" required="true" />
        <cfargument name="forumId" type="numeric" required="true" />

        <cfset var qAddThread = "" />
        <cfset var qAddMessage = "" />
        <cfset var success = true />

        <!--- add thread data to threads table --->
        <cftry>
            <cfquery name="qAddThread" datasource="galleon">
                SET NOCOUNT ON
                INSERT INTO threads (
                    name, 
                    readonly, 
                    active, 
                    forumidfk, 
                    useridfk, 
                    datecreated
                ) VALUES (
                    <cfqueryparam value="#arguments.threadName#" cfsqltype="cf_sql_varchar"
					 maxlength="100" />, 
                    <cfqueryparam value="0" cfsqltype="cf_sql_bit" />, 
                    <cfqueryparam value="1" cfsqltype="cf_sql_bit" />, 
                    <cfqueryparam value="#arguments.forumId#" cfsqltype="cf_sql_integer" />, 
                    <cfqueryparam value="3" cfsqltype="cf_sql_integer" />, 
                    <cfqueryparam value="#Now()#" cfsqltype="cf_sql_timestamp" />
                )
                SELECT @@IDENTITY AS newId;
                SET NOCOUNT OFF
            </cfquery>
            <cfcatch type="database">
                <cfset success = false />
            </cfcatch>
        </cftry>

        <!--- since this is a new thread, add a message to the messages table as well --->
        <cfif success>
            <cftry>
                <cfset success = addMessage(arguments.threadName, arguments.threadBody,
				 qAddThread.newId) />
                <cfcatch type="any">
                    <cfset success = false />
                </cfcatch>
            </cftry>
        </cfif>
    </cffunction>

    <!--- MESSAGE METHODS --->
    <cffunction name="getMessages" access="remote" returntype="query" output="false" hint="Returns
	 a list of messages.">
        <cfargument name="threadid" type="numeric" required="false" default="0" />

        <cfset var qGetMessages = "" />

        <cfquery name="qGetMessages" datasource="galleon">
            SELECT messages.id, messages.title, 
                messages.posted, messages.threadidfk, messages.useridfk, 
                threads.name as threadname, users.username
             FROM messages, threads, users
            WHERE messages.threadidfk *= threads.id
            AND messages.useridfk *= users.id
            <cfif arguments.threadid gt 0>
                AND messages.threadidfk = <cfqueryparam value="#arguments.threadid#"
				 cfsqltype="cf_sql_integer" />
            </cfif>
            ORDER BY posted ASC
        </cfquery>

        <cfreturn qGetMessages />
    </cffunction>

    <cffunction name="getMessage" access="remote" returntype="query" output="false" hint="Returns
	 a single message.">
        <cfargument name="messageid" type="numeric" required="true" />

        <cfset var qGetMessage = "" />

        <cfquery name="qGetMessage" datasource="galleon">
            SELECT messages.title, messages.body, messages.posted, messages.useridfk, 
                users.username, users.datecreated 
            FROM messages, users 
            WHERE messages.useridfk *= users.id 
            AND messages.id = <cfqueryparam value="#arguments.messageid#"
			 cfsqltype="cf_sql_integer" />
        </cfquery>

        <cfreturn qGetMessage />
    </cffunction>

    <cffunction name="addMessage" access="remote" returntype="void" output="false" hint="Adds a
	 new message.">
        <cfargument name="messageTitle" type="string" required="true" />
        <cfargument name="messageBody" type="string" required="true" />
        <cfargument name="threadId" type="numeric" required="true" />

        <cfset var qAddMessage = "" />

        <cfquery name="qAddMessage" datasource="galleon">
            SET NOCOUNT ON
            INSERT INTO messages (
                title, 
                body, 
                useridfk, 
                threadidfk, 
                posted
            ) VALUES (
                <cfqueryparam value="#arguments.messageTitle#" cfsqltype="cf_sql_varchar"
				 maxlength="100" />, 
                <cfqueryparam value="#arguments.messageBody#" cfsqltype="cf_sql_longvarchar" />, 
                <cfqueryparam value="3" cfsqltype="cf_sql_integer" />, 
                <cfqueryparam value="#arguments.threadId#" cfsqltype="cf_sql_integer" />, 
                <cfqueryparam value="#Now()#" cfsqltype="cf_sql_timestamp" />
            )
            SELECT @@IDENTITY AS newID;
            SET NOCOUNT OFF
        </cfquery>
    </cffunction>
</cfcomponent>

LISTING 2: getConferences.cfm

<cfsetting enablecfoutputonly="yes" />
<cfquery name="qGetConferences" datasource="galleon">
    SELECT conferences.id, 
        conferences.name, 
        conferences.description, 
        forums.id AS forumId, 
        forums.name AS forumName, 
        forums.description AS forumDescription, 
        forums.readonly
    FROM conferences 
    LEFT JOIN forums 
    ON conferences.id = forums.conferenceidfk 
    ORDER BY conferences.name, forumName ASC
</cfquery>

<cfif qGetConferences.RecordCount GT 0>
    <!--- build xml to provide to Flex Tree --->
    <cfoutput>
        <cfxml variable="conferences">
            <node>
            <cfloop index="i" from="1" to="#qGetConferences.RecordCount#" step="1">
                <cfif i EQ 1 OR (i GT 1 AND qGetConferences["name"][i] NEQ
				 qGetConferences["name"][i -1])>
                    <node label="#qGetConferences['name'][i]#" data="#qGetConferences['id'][i]#"
					 description="#qGetConferences['description'][i]#" type="conference">
                    <node label="#qGetConferences['forumName'][i]#"
					 data="#qGetConferences['forumId'][i]#"
					  description="#qGetConferences['forumDescription'][i]#" type="forum" />
                    <cfelse>
                        <node label="#qGetConferences['forumName'][i]#"
				 data="#qGetConferences['forumId'][i]#"
				 description="#qGetConferences['forumDescription'][i]#" type="forum" />
                </cfif>
                <cfif (i + 1 LTE qGetConferences.RecordCount AND qGetConferences["name"][i + 1]
				 NEQ qGetConferences["name"][i]) OR i EQ qGetConferences.RecordCount>
                    </node>
                </cfif>
            </cfloop>
            </node>
        </cfxml>
    </cfoutput>

    <!--- no conference/forum data retrieved, so build dummy xml that indicates this --->
    <cfelse>
        <cfoutput>
            <cfxml variable="conferences">
                <node>
                    <node label="No Data Available" data="0" description="No data was retrieved
					 from the server." type="conference" />
                </node>
            </cfxml>
        </cfoutput>
</cfif>

<cfoutput>#conferences#</cfoutput>

LISTING 3: FlexForums.mxml

<?xml version="1.0" encoding="utf-8"?>
<!-- the main application tag; the initialize attribute tells the application to call
        the getConferences function as the application initialized so we have data in the 
        navigation tree immediately after the application loads -->
<mx:Application xmlns:mx="http://www.macromedia.com/2003/mxml" pageTitle="Flex Forums" initialize="getConferences();">

    <!-- include the ActionScript functions -->
    <mx:Script source="FlexForums_script.as" />

    <!-- create handle for getConferences.cfm file -->
    <mx:HTTPService id="conferenceFeed"
	 url="http://192.168.1.105:8103/cfusion/forums/getConferences.cfm" showBusyCursor="true" />

    <!-- create handle for FlexForums.cfc; this is a named service in the Flex whitelist -->
    <mx:WebService id="forumWS" serviceName="FlexForums" showBusyCursor="true">
        <!-- declare these methods of the CFC specificially so we can tell it
                to refresh the display upon completion of these method calls -->
        <mx:operation name="addMessage" result="refresh()" />
        <mx:operation name="addThread" result="refresh()" />
    </mx:WebService>

    <!-- the main application panel -->
    <mx:Panel id="mainPanel" width="100%" height="100%" title="Flex Forums">
        <!-- horizontal divided box that allows user to adjust width between the left tree
                and the right-hand application panel -->
        <mx:HDividedBox id="leftNav" width="100%" height="100%">
            <!-- the navigation tree; data comes from call to the conferenceFeed
                    HTTPService, which in turn calls the getConferences.cfm page -->
            <mx:Tree id="forumTree" height="100%" width="20%" showDataTips="true"
			 dataTipField="description" change="handleTreeChange(event);"
			  dataProvider="{conferenceFeed.result.node}" />

            <!-- vertical box that contains the right-hand area of the application -->
            <mx:VBox width="80%" height="100%">
                <!-- horizontal box to contain the view stack control, navigation labels, and
                        refresh button -->
                <mx:HBox height="28" width="100%" verticalAlign="middle">
                    <!-- horizontal box to contain just the view stack control and
                            navigation labels -->
                    <mx:HBox width="100%" verticalAlign="middle">
                        <!-- link bar controls the thread/message viewstack -->
                        <mx:LinkBar dataProvider="gridViewStack" borderStyle="solid" />
                        <!-- text labels to show the user where they are -->
                        <mx:Label id="forumTitle" text="Please Select a Forum From the Tree On the
						 Left" />
                        <mx:Label id="threadTitle" text="" />
                    </mx:HBox>

                    <!-- horizontal box for the refresh button so we can have it right align -->
                    <mx:HBox verticalAlign="middle" horizontalAlign="right">
                        <mx:Link id="refreshLink" icon="@Embed('images/syncicon.png')"
						 click="refresh()" toolTip="Refresh Content" />
                    </mx:HBox>
                </mx:HBox>

                <!-- the view stack that toggles between the thread and message views -->
                <mx:ViewStack width="100%" height="100%" id="gridViewStack">
                    <!-- vertical box for thread data grid and new thread button at bottom -->
                    <mx:VBox id="threads" label="Threads" width="100%" height="100%">
                        <!-- the data grid for the thread data -->
                        <mx:DataGrid id="threadGrid" width="100%" height="100%"
						 dataProvider="{forumWS.getThreads.result}"
						  change="threadSelected(threadGrid.selectedItem.ID,
						   threadGrid.selectedItem.NAME)">
                            <mx:columns>
                                <mx:Array>
                                    <mx:DataGridColumn columnName="NAME" headerText="Thread" />
        <mx:DataGridColumn columnName="USERNAME" headerText="Originator" width="40" />
        <mx:DataGridColumn columnName="MESSAGECOUNT" headerText="Replies" width="40" />
        <mx:DataGridColumn columnName="LASTPOST" headerText="Last Post" width="70" />
                                </mx:Array>
                            </mx:columns>
                        </mx:DataGrid>

                        <!-- horizontal box below data grid for new thread button -->
                        <mx:HBox id="threadButtonBox" width="100%" height="40"
						 horizontalAlign="right" verticalAlign="middle">
                            <mx:Button id="newThreadButton" label="New Thread"
							 click="showNewThreadForm();" visible="false" />
                        </mx:HBox>
                   </mx:VBox>

                    <!-- vertical divided box for message list and details allows user to control
                            height between the message data grid and the message detail area -->
                    <mx:VDividedBox width="100%" height="100%" id="messages" label="Messages">
                        <!-- the data grid for the message data -->
                        <mx:DataGrid id="messageGrid" width="100%" height="50%"
						 dataProvider="{forumWS.getMessages.result}"
						  change="showMessage(messageGrid.selectedItem.ID);">
                            <mx:columns>
                                <mx:Array>
                                    <mx:DataGridColumn columnName="TITLE" headerText="Title" />
                                    <mx:DataGridColumn columnName="USERNAME" headerText="Posted
									 By" width="40" />
                                    <mx:DataGridColumn columnName="POSTED" headerText="Posted On"
									 width="50" />
                                </mx:Array>
                            </mx:columns>
                        </mx:DataGrid>

                        <!-- vertical box for the message headers and the message contents -->
                        <mx:VBox height="50%" width="100%">
      <mx:HBox id="messageHeader" height="30" width="100%" visible="false" verticalAlign="bottom">
      <mx:Label htmlText="<b>Posted by:</b>
	  {forumWS.getMessage.result[0].USERNAME}" />
      <mx:Label htmlText="<b>Posted on:</b>
	  {forumWS.getMessage.result[0].POSTED}" width="400" />
                             </mx:HBox>

                            <!-- text area displays the message content; note that when a CF query
                                    is returned to Flex it comes back as an array with one array
                                    element per query record -->
                            <mx:TextArea id="messageDetails" width="100%" height="100%"
							 wordWrap="true" editable="false"
							  text="{forumWS.getMessage.result[0].BODY}" />

                            <!-- horizontal box for message buttons (new and reply) -->
                            <mx:HBox id="messageButtonBox" width="100%" height="40"
							 horizontalAlign="right" verticalAlign="middle">
         <mx:Button id="replyMessageButton" label="Reply"
		 click="showMessageForm(true);" visible="false" />
         <mx:Button id="newMessageButton" label="New Message"
		 click="showMessageForm(false);" />
                            </mx:HBox>
                        </mx:VBox>
                    </mx:VDividedBox>
                </mx:ViewStack>
            </mx:VBox>
        </mx:HDividedBox>
    </mx:Panel>
</mx:Application>

LISTING 4: FlexForums_script.as

// get conference data for tree
function getConferences() {
    conferenceFeed.send();
}

// handle tree change
function handleTreeChange(event) {
    /* Get the type of item the user clicked on (forum or conference). If it's a
    conference, we don't do anything, but if it's a forum, we display the threads. */
    var type = event.target.selectedItem.getProperty("type");

    // if user clicked on a forum, load the messages
    if (type == "forum") {
        // set forumId from the id of the item the user selected in the tree
        var forumId = event.target.selectedItem.getData();

        // set label above grids to forum name
        forumTitle.text = event.target.selectedItem.getProperty("label");
        threadTitle.text = "";

        // set selected item in view stack to threads, clear out old data, and show new
        // thread button
        gridViewStack.selectedChild = threads;
        newThreadButton.visible = true;
        messageGrid.removeAll();
        messageDetails.htmlText = "";
        messageHeader.visible = false;

        // load the threads; the result of this call is bound to the datagrid
        if (forumId != undefined) {
            forumWS.getThreads(forumId);
        }
    }
}

// item clicked in thread grid, so load messages
function threadSelected(threadId, title) {
    // the result of this call is bound to the datagrid
    forumWS.getMessages(threadId);
    gridViewStack.selectedChild = messages;
    threadTitle.text = ">  " + title;

    // clear out any existing message data
    messageHeader.visible = false;
    messageDetails.text = "";
}

// item clicked in message grid, so show message
function showMessage(messageId) {
    // this returns a cf query which comes back to flex as an array with one array element
    // per row
    forumWS.getMessage(messageId);

    // show message header and reply button
    messageHeader.visible = true;
    replyMessageButton.visible = true;
}

// show new thread form in popup window
function showNewThreadForm() {
    var threadForm = mx.managers.PopUpManager.createPopUp(_root, ThreadForm, true, {deferred: true});
}

    /* add new thread; the result attribute of the mx:operation tag in the main
        MXML file will refresh the thread grid once the add operation has completed */
    function addThread(title, body) {
        forumWS.addThread(title, body, forumTree.selectedItem.getData());
}

// show message form in popup window; reply boolean indicates whether this is a reply
// or a new message
function showMessageForm(reply) {
    // open the popup
    var messageForm = mx.managers.PopUpManager.createPopUp(_root, MessageForm, true,
	 {deferred:true});

    // if this is a reply as opposed to a new message, pre-populate the subject field
    if (reply) {
        var subject = "";

        // only add RE: to subject if it isn't already there
        if (messageGrid.selectedItem.TITLE.substring(0, 3) != "RE:") {
            subject = "RE: " + messageGrid.selectedItem.TITLE;
        } else {
            subject = messageGrid.selectedItem.TITLE;
        }

        messageForm.messageSubject.text = subject;
    }
}

// add message
function addMessage(subject, body) {
    forumWS.addMessage(subject, body, threadGrid.selectedItem.ID);
}

// refresh messages and threads as needed
function refresh() {
    if (gridViewStack.selectedChild == threads && forumTree.selectedItem.getData() != undefined) {
        forumWS.getThreads(forumTree.selectedItem.getData());
    } else if (gridViewStack.selectedChild == messages) {
        forumWS.getMessages(threadGrid.selectedItem.ID);
    }
}

LISTING 5: ThreadForm.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:TitleWindow xmlns:mx="http://www.macromedia.com/2003/mxml" title="New Thread">
    <mx:Form>
        <mx:FormItem label="Title" required="true">
            <mx:TextInput id="threadTitle" width="300" />
        </mx:FormItem>

        <mx:FormItem label="Body" required="true">
            <mx:TextArea id="threadBody" height="300" width="300" wordWrap="true" />
        </mx:FormItem>
    </mx:Form>

    <mx:ControlBar horizontalAlign="right">
        <mx:Button label="Cancel" click="this.deletePopUp();" />
        <mx:Button label="Submit" click="_root.addThread(threadTitle.text,
		 threadBody.text);this.deletePopUp();" />
    </mx:ControlBar>
</mx:TitleWindow>

LISTING 6: MessageForm.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:TitleWindow xmlns:mx="http://www.macromedia.com/2003/mxml" title="Post Message">
    <mx:Form>
        <mx:FormItem label="Subject" required="true">
            <mx:TextInput id="messageSubject" width="300" />
        </mx:FormItem>

        <mx:FormItem label="Body" required="true">
            <mx:TextArea id="messageBody" width="300" height="300" wordWrap="true" />
        </mx:FormItem>
    </mx:Form>

    <mx:ControlBar horizontalAlign="right">
        <mx:Button label="Cancel" click="this.deletePopUp();" />
        <mx:Button label="Submit" click="_root.addMessage(messageSubject.text,
		 messageBody.text);this.deletePopUp();" />
    </mx:ControlBar>
</mx:TitleWindow>

Listing 7: Use capital letters to refer to the column names

<mx:DataGrid id="threadGrid" width="100%" height="100%" dataProvider="{forumWS.getThreads.result}"
 change="threadSelected(threadGrid.selectedItem.ID, threadGrid.selectedItem.NAME)">
    <mx:columns>
        <mx:Array>
            <mx:DataGridColumn columnName="NAME" headerText="Thread" />
            <mx:DataGridColumn columnName="USERNAME" headerText="Originator" width="40" />
            <mx:DataGridColumn columnName="MESSAGECOUNT" headerText="Replies" width="40" />
            <mx:DataGridColumn columnName="LASTPOST" headerText="Last Post" width="70" />
        </mx:Array>
    </mx:columns>
</mx:DataGrid>