Move your desktop software into the browser

October 28, 2007

Article Source :http://www.adobe.com/devnet/flash/articles/move_to_browser.html

One of the most astounding aspects of building Rich Internet Applications based on Macromedia ColdFusion MX and Macromedia Flash MX, connected through Macromedia Flash Remoting, is the ease with which common desktop software functionalities and complex user interactions can be replicated into web-based applications. The benefits of developing such applications are countless, starting from the clear distinction between business logic (wrapped inside ColdFusion components) and graphical user interface (designed in Macromedia Flash), to reaching a larger audience and many different devices thanks to the ubiquity of a rich client—Macromedia Flash Player.

The following tutorial guides you through the implementation of a simple FTP client that takes advantage of the built-in file transfer capabilities of ColdFusion MX to access FTP servers—and of the Flash Tree component, part of the Flash UI Components Set 2 available from the Macromedia Exchange—to display and handle the data retrieved by ColdFusion MX.

Getting the software and sample files
First make sure you have the required software and that it is correctly installed:

Next download the sample files to follow along with this tutorial:

Windows: flash_ftp.zip (200K)

Macintosh: flash_ftp.sit (140K)

Now create a folder at cf_webroot\com\aftershape\ftp and unzip/expand the ftpSpider.cfc ColdFusion MX component into the ftp directory.

Note: The cf_webroot directory path depends on your ColdFusion MX installation.

The flash_ftp.fla file is the finished Flash graphic user interface (GUI). This file will help you follow the tutorial if you’d like to study the finished project.

The flash_ftp_start.fla file contains everything except the ActionScript code—which is explained during this tutorial. Use this file if you’d like to add the ActionScript yourself as it is presented.

How the ftpSpider component works
All the tasks performed on the remote FTP server are handled by the ftpSpider ColdFusion MX component. This component uses the CFFTP tag, so make sure it has been enabled in the ColdFusion Administrator.

The component is composed of two CFFUNCTION tags which define its two methods. The first, named FetchDirectories, handles all the files and directories listing operations. The second, named FetchFiles, handles transferring operations from the remote FTP server to the temporary directory of your ColdFusion MX server. To make these methods available to the Flash GUI, which I build in the second part of the tutorial, you need to set the attribute access to “remote” for both CFFUNCTION tags that form the component.

Using the FetchDirectories method
To use the component’s first method, gather the needed FTP parameters using a succession of CFARGUMENTS tags. Each FTP parameter is collected and sent to the ColdFusion component by the Flash GUI, as you’ll see later on. These parameters are the main attributes required by the CFFTP tag:

<cfargument name="ftpUsername" type="any" required="yes">
<cfargument name="ftpPassword" type="any" required="yes">
<cfargument name="ftpServer" type="any" required="yes">
<cfargument name="ftpPort" type="numeric" required="yes">
<cfargument name="ftpPassivemode" type="boolean" required="yes">
<cfargument name="ftpTimeout" type="numeric" required="yes">
<cfargument name="ftpDirectory" type="any" required="yes">

Once you’ve defined all the FTP parameters, you can start connecting to the remote FTP server using another CFFTP tag, with the action attribute set to OPEN. You will benefit from the connection-caching feature of ColdFusion, which automatically defines a connection name on the connection attribute. This feature enables you to reuse the same connection information on successive CFFTP tags:

<cfftp action="OPEN"
server="#ARGUMENTS.ftpServer#"
username="#ARGUMENTS.ftpUsername#"
password="#ARGUMENTS.ftpPassword#"
stoponerror="No"
passive="#ARGUMENTS.ftpPassivemode#"
port="#ARGUMENTS.ftpPort#"
connection="MyFTPConnection"
retrycount="3"
timeout="#ARGUMENTS.ftpTimeout#">

From the remote FTP server, retrieve all names and attributes of the stored directories and files as a query object using the LISTDIR action of the CFFTP. After that, send this information to the calling Flash interface using the CFRETURN tag. Once you’ve done this, you can close the FTP connection to avoid multiple open connections each time you process the ColdFusion component:

<cfftp action="LISTDIR"
stoponerror="No"
passive="#ARGUMENTS.ftpPassivemode#"
name="FileList"
directory="#ARGUMENTS.ftpDirectory#"
connection="MyFTPConnection">
<cfftp action="CLOSE"
stoponerror="No"
passive="#ARGUMENTS.ftpPassivemode#"
connection="MyFTPConnection">
<cfreturn FileList>

Using the FetchFiles method
The second method of the component works almost the same way as FetchDirectories. The only relevant difference is that now, because the connection has already been established, you will download the file selected on the Flash GUI to your ColdFusion MX temporary folder instead of retrieving a list of directories. You can accomplish this task easily by setting the action attribute of the CFFTP tag to GETFILE. The user will be notified about the final result of the download operation by the CFFTP.

In the code example below, the succeeded variable—which is populated by assigning a value of No to the stoponerror attribute of the CFFTP tag—is subsequently returned to the Flash GUI by the CFRETURN tag:

<cfftp action="GETFILE"
stoponerror="No"
passive="#ARGUMENTS.ftpPassivemode#"
localfile="#GetTempDirectory()##ARGUMENTS.ftpFileLocal#"
remotefile="#ARGUMENTS.ftpFileRemote#"
transfermode="AUTO"
failifexists="Yes"
connection="MyFTPConnection">
<cfreturn CFFTP.Succeeded>

Note: Because of its single-threaded nature, it’s always a good idea to lock the CFFTP tag. However, a digression on locking was beyond the scope of this tutorial. For the sake of simplicity, each FTP operation is not locked and performs on separate CFFTP tags—even if you could, for example, open the connection to the remote FTP server and download the desired file in a single step. For more information on locking, please refer to Macromedia’s TechNote, “ColdFusion Locking Best Practices.”

Building the Flash FTP GUI
Open the flash_ftp_start.fla file and take a second to examine it. As you can see, all the elements appear on the Stage for you. The only blank frame is on the top layer, which is named “actions.”

There are several input text fields at the top right of the Stage. The user enters the login information into these text fields to connect to the remote FTP server.

The PushButton Flash component labeled “Connect” passes the values gathered from the text fields to the ConnectToFTP function you are going to write in a few minutes. That function is responsible for starting the whole application.

Below the PushButton component, there’s another dynamic text field named “FieldServerMsg_txt”; this is where you display custom error messages and the status of each FTP operation.

The left side of the Stage is where the Tree Flash component resides. FtpBrowser_tree is responsible for displaying the remote FTP server files. Each time a tree node is selected, this component calls a function named GetSelection, which is one of the Tree component’s behaviors.

Select the blank top layer and open the Actions panel by pressing F9 or selecting Window > Actions. Once it’s opened, make sure your Script pane is set to Expert Mode. Start by including the NetService.as file, which contains the classes required by Flash Remoting:

#include "NetServices.as"

The first function you implement is the one that changes the status of the selected tree node from open to closed (and vice versa). This function is only called when the selected node is a branch that corresponds to a folder on the remote FTP server.

Note the use of suffixes such as _tn (which stands for “tree node”) and _tree. I recommend using suffix strings like these whenever writing ActionScript because the code completion support built into Macromedia Flash MX displays code hints:

function OpenNode(state) {
CurrentNode_tn.setIsOpen(state);
FtpBrowser_tree.refresh();
}

To handle the returned data from the service call to the FetchDirectories method of your ColdFusion component, set up a default responder. The function will populate the Tree Flash component depending on the query object received from the spiderFtp.cfc file.

First, it will check to see if an empty query has been returned. When the query contains one or more records, it will loop through them and add the tree branch icon (if the processed record is a directory) or a leaf icon (if it is a file). Each tree node will be labeled with the name of the column returned by the ColdFusion component, while the path column will define the node’s data value.

Notice that both the name and path columns are standard CFFTP query object columns. CurrentNode_tn is nothing but the directory used as a base folder for each call to the ColdFusion component. Once the user begins browsing the tree, CurrentNode_tn will be replaced by the selected tree node data value. To provide visual feedback to the user, the selected branch status will be set to open each time the result of the service call would have been processed—even if the returning query object is empty:

function FetchDirectories_Result(result) {
// If the query returns no record...
if (result.getLength() == 0) {
// ...directory is empty. Display message and open the branch.
FieldServerMsg_txt.text = "Directory is empty";
OpenNode(true);
} else {
// Loop through query result...
for (i=0; i<result.getLength(); i++) {
// ...if a directory is found...
if (result.getItemAt(i).isDirectory == "YES") {
// ...add it as a branch.
FtpBrowser_tree.addNode(CurrentNode_tn, new FTreeNode(result.getItemAt(i).name).setData(result.getItemAt(i).path).setIsBranch());
// ...and open parent branch
OpenNode(true);
FieldServerMsg_txt.text = "";
} else {
// Else add it as a leaf...
FtpBrowser_tree.addNode(CurrentNode_tn, new FTreeNode(result.getItemAt(i).name).setData(result.getItemAt(i).path));
// ...and open parent branch
OpenNode(true);
FieldServerMsg_txt.text = "";
}
}
}
}

 

To handle returned errors from the FetchDirectories service function, set up a simple responder that displays the error messages in the FieldServerMsg_txt dynamic text field on the Stage:

function FetchDirectories_Status(result) {
// Print error messages
FieldServerMsg_txt.text = result.details;
}

The default responder for the FetchFiles service function is straightforward. The FTP connection errors are handled by the FetchDirectories service function error responder, which is very helpful. In fact, users are able to browse the remote FTP server and choose a file that they wish to download as long as the login information they entered to call the FetchDirectories function is correct—you don’t have to worry about connection errors.

The CFFTP variable, which you receive from the ColdFusion components, returns a simple Boolean value depending on the download process result. You only need to display a status message on the FieldServerMsg_txt dynamic text field to provide users with feedback on the process:

function FetchFiles_Result(result) {
if (result) {
FieldServerMsg_txt.text = "File has been downloaded to your ColdFusion temporary directory.";
} else {
FieldServerMsg_txt.text = "Error occurred while trying to download the selected file.";
}
}

The CreateRootNode function is called only once by the PushButton Flash component. It creates the root node of the Tree Flash component and makes the first call to the FetchDirectories service:

function CreateRootNode() {
RootNode_tn = new FTreeNode("/").setData("/");
CurrentNode_tn = RootNode_tn;
CurrentNode_tn.setIsBranch();
FtpBrowser_tree.setRootNode(CurrentNode_tn);
// Retrieve directories list
DirectoryService.FetchDirectories(FieldUsername_txt.text, FieldPassword_txt.text, FieldServer_txt.text, FieldPort_txt.text, PassiveMode_cb.getValue(), FieldTimeout_txt.text, CurrentNode_tn.getData());
FieldServerMsg_txt.text = "Retrieving files list...";
}

The function called by the PushButton Flash component on the Stage simply resets the message text field and calls the CreateRootNode function I discussed above:

function ConnectToFTP() {
// Reset messages window...
FieldServerMsg_txt.text = "";
// ...and call CreateRootNode function
CreateRootNode();
}

 

The GetSelection function is responsible for the Tree Flash component’s behavior. It sets the value for CurrentNode_tn with the current selected node on the Tree. Also, if the selected node is a branch, it opens or closes depending on the branch’s current status. The function calls the FetchDirectories service if it has no child nodes; otherwise it calls the FetchFiles service if the selected node is a leaf that represents a file on the remote FTP server:

function GetSelection() {
// Define current selected node
CurrentNode_tn = FtpBrowser_tree.getSelectedNode();
// The following routine opens or closes branches depending on their status or number of children
if (CurrentNode_tn.isBranch()) {
if (CurrentNode_tn.getNumChildren() == 0) {
DirectoryService.FetchDirectories(FieldUsername_txt.text, FieldPassword_txt.text, FieldServer_txt.text, FieldPort_txt.text, PassiveMode_cb.getValue(), FieldTimeout_txt.text, CurrentNode_tn.getData());
FieldServerMsg_txt.text = "Retrieving files list...";
} else {
if (CurrentNode_tn.isOpen()) {
OpenNode(false);
} else {
OpenNode(true);
}
}
// If a leaf has been selected instead of a branch, start the download
} else {
DirectoryService.FetchFiles(FieldUsername_txt.text, FieldPassword_txt.text, FieldServer_txt.text, FieldPort_txt.text, PassiveMode_cb.getValue(), FieldTimeout_txt.text, CurrentNode_tn.getLabel(), CurrentNode_tn.getData());
FieldServerMsg_txt.text = "Downloading file. Please wait.";
}
}

Last, but not least, comes the initialization code. I usually leave this task for the end of my ActionScript development so that I’m sure all my functions have been loaded before processing it.

The initialization code simply contains the URL to the default gateway, the construction of the main Service Object, and some customization methods of the assets on the main Stage. Remember that the URL to your default gateway depends on your installation. In this example, the URL points to the default gateway on the stand-alone web server that comes with ColdFusion MX:

if (init == null) {
init = true;
// Set the default gateway
NetServices.setDefaultGatewayUrl("http://localhost:8500/flashservices/gateway");
// Make the Gateway connection
GatewayConnection_nc = NetServices.createGatewayConnection();
// Create Service Object
DirectoryService = GatewayConnection_nc.getService("com.aftershape.ftp.ftpSpider", this);
// Set autohide to tree's scrollbar
FtpBrowser_tree.setAutoHideScrollBar(true);
// ::trick:: remove expander symbol linking to an object that doesn't exist
FtpBrowser_tree.setExpanderSymbolName("null");
// Set default ftp settings
FieldPort_txt.text = 21;
FieldTimeout_txt.text = 50;
// Set tab index order
FieldServer_txt.tabIndex = 1;
FieldPort_txt.tabIndex = 2;
FieldUsername_txt.tabIndex = 3;
FieldPassword_txt.tabIndex = 4;
FieldTimeout_txt.tabIndex = 5;
FieldServerMsg_txt.tabEnabled = false;
}

This tutorial demonstrates how Rich Internet Applications can extend current Internet applications by providing developers with the tools they need to easily replicate common desktop software user interactions and functionalities. You can use this example as a starting point to build a more complete and sophisticated FTP client, or as inspiration for more compelling applications.


Building a Better Custom Tag

October 27, 2007

Building a Better Custom Tag

By: Ben Forta

We’ve come a long way in the past couple of years. Not that long ago I was teaching how to write simple custom tags and encouraging developers to experiment with them.

Now Allaire’s Developers Exchange has thousands of custom tags listed, and I see developers using them as part of everyday development. Considering that it’s been only a few years since Jeremy Allaire asked me to write a few tags so his new Tag Gallery would contain some initial content, we’ve made real progress.

So now I’d like to up the ante a bit and challenge developers to build a better mousetrap (so to speak). Most of the newly available tags are new twists on old ideas, often the same old way of doing things. I’d like to see developers get creative with tag designs, making them as flexible and as reusable as possible. The better the tag design, the better the abstraction, the better the encapsulation, the better the interface – the more uses you’ll find for it. That’s really what custom tags are all about.

Building Basic Tags
The best way to explain what I mean is with an example, so here goes.

I’ve been doing lots of WAP work recently (as I know many of you have). When generating WAP content you’ve probably discovered that $ (the dollar sign) is a special character in WML – it’s used to prefix variables. Any time $ is used in text that’s not a variable, it has to be escaped as $$ (kind of like # is escaped as ## in CFML). I found myself using code like this throughout my application:

#Replace(variable, “$”, “$$”, “ALL”)#

This Replace() function simply replaces all “$” with “$$”. Now, when CF5 ships I’ll create a user-defined function called WAPSafe() that will likely look something like this:

<CFSCRIPT>
function WAPSafe(string)
{
return Replace(string, “$”, “$$”, “ALL”);
} </CFSCRIPT>

I’d then be able to simply format text as follows:

#WAPSafe(var)#

But user-defined functions are not available yet, so I created a simple custom tag using the code shown below:

<!— Initialize defaults —>
<CFPARAM NAME=”ATTRIBUTES.text” DEFAULT=”">
<!— Process text —>
<CFOUTPUT>#Replace(ATTRIBUTES.text, “$”, “$$”, “ALL”)#</CFOUTPUT>

This tag, named <CF_WAPSafe>, takes a single attribute that contains the text to be processed. The code within the tag simply displays the processed text. To call this tag I’d do something like this: <CF_WAPSafe TEXT=”#var#”> So far so good.

Dual Purpose Tags
Custom tags that process data shouldn’t arbitrarily write output text. For maximum control, custom tags should return results to the caller page and let the caller code do whatever it needs with the data.

Here’s the revised <CF_WAP-Safe> tag:

<!— Initialize defaults —>
<CFPARAM NAME=”ATTRIBUTES.text” DEFAULT=”">
<CFPARAM NAME=”ATTRIBUTES.variable” DEFAULT=”WAPSAFE”>
<!— Process text —>
<CFSET output=Replace(ATTRIBUTES.text, “$”, “$$”, “ALL”)>

<!— Save to CALLER variable —>
<CFSET “CALLER.#ATTRIBUTES.variable#”=output>

The tag takes two attributes: the text to be processed and the name of a variable to be created in the CALLER scope (arbitrarily naming variables is bad form; the caller should always be able to specify the name of the variable to be created). The custom tag processes the data and then saves the converted text into a variable. To use this tag I’d do the following:

<CF_WAPSafe TEXT=”#var#” VARIABLE=”wap_var”>
<CFOUTPUT>#wap_var#</CFOUTPUT>

There are now two versions of the same tag: one that saves converted text to a variable and one that displays it. Even though best practices dictate that saving the results to a variable is preferred, there may be occasions when I’d want to have the processed text dumped to the output. So why not support both modes of operation as follows?

<!— Initialize defaults —>
<CFPARAM NAME=”ATTRIBUTES.text” DEFAULT=”">
<CFPARAM NAME=”ATTRIBUTES.variable” DEFAULT=”">
<!— Process text —>
<CFSET output=Replace(ATTRIBUTES.text, “$”, “$$”, “ALL”)>

<!— Check if VARIABLE passed —>
<CFIF ATTRIBUTES.variable IS “”>
<!— Display it —>
<CFOUTPUT>#output#</CFOUTPUT>
<CFELSE>
<!— Save to CALLER variable —>
<CFSET “CALLER.#ATTRIBUTES.variable#”=output>
</CFIF>

This custom tag is the same as the previous version except that the final <CFIF> statement checks if the VARIABLE attribute was specified. If it was, the processed data is saved to the specified variable; otherwise it’s displayed using a regular <CFOUTPUT> block.

With minimal extra work the custom tag is now a bit more useful.

The Tag Pair Solution
There’s another version of this custom tag that would be useful. Instead of passing the text to be processed as a variable, it would be nice to be able to simply enclose it within tags, such as:

<CF_WAPSafe>
..
</CF_WAPSafe>

This way I could use all sorts of functions, expressions, tags, whatever I needed, and be sure that the generated output was safely formatted. ColdFusion makes writing this type of custom tag very easy; here’s the code for the new version:

<!— Only process in End mode —>
<CFIF ThisTag.ExecutionMode IS “End”>
<!— Process text —>
<CFSET output=Replace(ThisTag. GeneratedContent, “$”, “$$”, “ALL”)>
<!— Update content —>
<CFSET ThisTag.GeneratedContent=output>
</CFIF>

This is a great example of using ColdFusion tag pairs. The tag itself is called twice, once when <CF_WAPSafe> (the start tag) is processed and again when </CF_WAPSafe> (the end tag) is processed. When the start tag is processed, the tag is called and ThisTag.ExecutionMode will be “Start”. When the end tag is processed, the tag will be called again and ThisTag.ExecutionMode will be “End”. Obviously, the custom tag needs to process the text when the end tag is reached (there would be nothing to process when the start tag is invoked) and so the entire code block is wrapped within a <CFIF> statement that checks that This-Tag.Execution-Mode is “End”.

The code itself is simple. This-Tag.GeneratedContent contains all the final postprocessing content present between the start and end tags, so Replace() processes that variable. Then ThisTag.Generated-Content is overwritten with the processed text.

Clean and simple.

Putting It All Together
I now have two versions of the tag: one that’s a single tag that takes passed data and one that’s used as a tag pair to process the enclosed content. Can these two tags be merged? Look at this code:

<!— Only process if stand alone or end of pair —>
<CFIF (NOT ThisTag.HasEndTag) OR (ThisTag.ExecutionMode IS “End”)>
<!— Initialize defaults —>
<CFPARAM NAME=”ATTRIBUTES.text” DEFAULT=”">
<CFPARAM NAME=”ATTRIBUTES.variable” DEFAULT=”">

<!— Get input —>
<CFIF ThisTag.HasEndTag AND ThisTag.ExecutionMode IS “End”>
<!— If tag pair get text between tags —>
<CFSET input=ThisTag.GeneratedContent>
<CFELSE>
<!— Otherwise use passed attribute —>
<CFSET input=ATTRIBUTES.text>
</CFIF>

<!— Process output text —>
<CFSET output=Replace(input, “$”, “$$”, “ALL”)>

<!— Check if VARIABLE passed —>
<CFIF ATTRIBUTES.variable IS “”>
<!— No variable passed —>
<CFIF ThisTag.HasEndTag>
<!— If tag pair update text between tags —>
<CFSET ThisTag.GeneratedContent=output>
<CFELSE>
<!— Otherwise display output —>
<CFOUTPUT>#output#</CFOUTPUT>
</CFIF>
<CFELSE>
<!— Save to CALLER variable —>
<CFSET “CALLER.#ATTRIBUTES.variable#”=output>
<!— If tag pair prevent display —>
<CFIF ThisTag.HasEndTag>
<CFSET ThisTag.GeneratedContent=”">
</CFIF>
</CFIF>
</CFIF>

The custom tag needs to be executed if the tag is called as a simple tag (not part of a tag pair) or as the end tag of a tag pair. The first <CFIF> statement tests for both of these conditions by checking This-Tag.Has-EndTag and ThisTag.Execution Mode. Next comes variable initialization. The text to be processed is retrieved from either the passed attribute or ThisTag.Generated-Content. Then the Replace() operation occurs. Once the text is processed, a determination is made as to whether the data is to be written out or saved to a variable. If the former, the data will be written to either ThisTag.Generated-Content or displayed as is. If the latter, This-Tag.GeneratedContent is flushed if needed.

The result? A single tag that can be used in four different ways. As a simple inline tag:

<CF_WAPSafe TEXT=”#var#”>

As a simple tag saving output to a specified variable:

<CF_WAPSafe TEXT=”#var#” VARIABLE=”output”>

As a tag pair replacing content automatically:

<CF_WAPSafe>
..
</CF_WAPSafe>

And one last option (that comes for free), to suppress output and write converted content to a variable:

<CF_WAPSafe VARIABLE=”output”>
..
</CF_WAPSafe>

I’ll be the first to admit that this simple example probably didn’t warrant such sophistication. The truth is, it’s probably overkill in this situation. I used this example because it’s a simple tag to understand (I didn’t want to dedicate pages to explaining the tag being built). The concepts and techniques used here are sound, and the more complex and sophisticated a custom tag is, the more it can benefit from this type of design. Where to Go from Here
I’ve only scratched the surface here. Tag families (parent-child tags) present even more opportunities. For example, consider a custom tag with syntax such as:

<CF_Chart NAME=”chart.jpg” QUERY=”data” X=”1000″ Y=”250″ Z=”350″ HEIGHT=”240″ WIDTH=”360″ COLOR=”white” TEXTCOLOR=”black”>

There’s nothing wrong with the syntax, but what if you were also to support the following:

<CF_Chart NAME=”chart.jpg” QUERY=”data”>
<CF_ChartParam NAME=”X” VALUE=”1000″>
<CF_ChartParam NAME=”Y” VALUE=”250″>
<CF_ChartParam NAME=”Z” VALUE=”350″>
<CF_ChartParam NAME=”HEIGHT” VALUE=”240″>
<CF_ChartParam NAME=”WIDTH” VALUE=”360″>
<CF_ChartParam NAME=”COLOR” VALUE=”white”>
<CF_ChartParam NAME=”TEXTCOLOR” VALUE=”black”>
</CF_Chart>

There are advantages and disadvantages to each syntax. The former is simpler to write, works well with Tag Editors, and is what new users will feel most comfortable with. The latter is cleaner, supports the conditional inclusion of attributes, and makes working with long lists of attributes much easier.

Which do you support? Why not both? The underlying tag code is the same in both cases; all that differs is how data is passed to the tag and any subsequent initialization code. If designed properly, every attribute should be supported both ways (of course you’ll need to find a way to handle conflicting values, but you should be handling that already even for simple tags).

Summary
As you can see, there’s a lot more to custom tags than a simple attribute passing. And there’s lots of room for you to be creative when designing your own custom tags. Custom tags are all about encapsulation, black-boxing, and reuse. Well-designed custom tags are powerful, flexible, intuitive, and highly usable. And most important, well-designed tags are used over and over. Are you up to the challenge?

Article Link : http://extentech.sys-con.com/read/41894.htm


Adobe ColdFusion 8

October 26, 2007

cf8_making_hard_things_easy_web_small.jpg

“Blazingly fast, packed with new features, integrated with everything from Java to .NET and Exchange, and it’s the brains behind Flex, Ajax and PDF applications – what’s not to love?
ColdFusion 8 is a must-have release.

Tim Buntel, Adobe senior product marketing manager