0.5.88 Released!

Below are the release notes for this latest version;

0.5.88 (10/6/2009) - Updated to work with CF 9 full release! (including samples)

- Now allow for custom-defined formats (via extension) and associated content types with new config option this.quicksilver.formats - example usage; this.quicksilver.formats = [{format='.bleh', contentType='text/html'}, {format='.blah', contentType='text/css'}];

- Added new built-in AOP aspect 'authFilter' which parses HTTP Basic authentication credentials when present (for rest web services), when that aspect is applied before a method invocation, two arguments are added to the runtime argument collection named: auth_username and auth_password which can be referenced / used in any subsequent method invocation.

- Fixed but where incorrect behavior was occurring when both webroot and application root were the same directory.

- Fixed a bug that was causing an error when returning JSON with no @returnTypeName annotation.

- Fixed an Application.cfc onMissingMethod issue that was causing incorrect behavior.

0.4.79 Released!

The latest beta release of Quicksilver is now available for download. Release notes are as follows;

- Created a new application configuration called this.quicksilver.viewRoot, can be overriden on the component level at any time.

- Improved performance by defaulting reload to 'false' - it now does not check to see if the framework needs to be reloaded on every request unless the reload key exists (this.quicksilver.reload = true).

- Added default behavior so when a map is not found, it will default to 'looking' for an associated .cfm view within the globalviewroot if no globalviewroot has been specified, then it will look for a view in the web root. There is no more need for component controller methods that do nothing more than render a view.

- Fixed an issue where the main method interceptor was only returning the AOP arguments structure and not the actual method return type.

- Fixed an issue with @returnTypeName annotated method where the value for that annotation was not being used properly to store the return results of its assocaited method in the framework argument collection.

- Streamlined naming convention of AOP decorated methods on a target object.

- Fixed an issue affecting AOP methods when in the case those methods would return structure data types, those structures were not being appended to the framework argument collection.

- Added cfide, cfdocs and web-inf to the list of default ignored packages.

- Updated sample apps.

- Removed some unused / unecessary files.

Creating Powerful SES / ReSTful URL's

/users
/user/12
/user/12.json
/hello/world.cfm
/articles/e-commerce/how_to_enable_google_analytics_on_your_site.html

Each of the URL's listed above are fully dynamic and can be easily configured to be supported by Quicksilver. There's a lot of value in the ability to configure a URL for an application that naturally expresses a logical location for a resource AND be highly dynamic without the excessive use of query-string variables. If you think your application can benefit from this capability, please read on.

Quicksilver implements a flexible and easy to use method of mapping SES (search-engine-safe) / ReST web service friendly URL's to your controller methods. In this tutorial, we're going to detail how to effectively leverage this feature to enhance your web application.

This feature is made possible by the following annotations;

@url
@httpMethod

These are method-level annotations, meaning that they are only relevant when attached to a specific method on a specific component, for example;

/**
* @url /hello/{text}
* @httpMethod GET
*/
public String function sayHello(required String text) {
var string = "Hello " & arguments.text;
return local.string;
}

The value succeeding the @url annotation is a URL matching "pattern" which can be comprised of several distinct elements. It's purpose is to describe a possible URL that will be matched to and invoke the associated method. For those of you who have had experience with PowerNap (one of my projects that facilitates the deployment of restful web services) the syntax of the url pattern should look familiar. The elements that can make up a url matching pattern are as follows;

String Literal: /hello/world
URL Variable: /{myVariable}
Reserved Keyword: {html} or {cfm}
Extension: .xml, .json, .html (or many, many others)

Here's the run down on each element and the role that each plays within the pattern you create. Before we go into detail though, it's important to understand that the URL matching patterns that you create can be comprised of any combination of the above elements allowing you to create URL's that fit the requirements of your application. Now on to the explanation.

String Literals
Specifying a string literal as a part of your URL matching pattern means that the URL must contain that exact text, in that exact location. For instance;

/hello/world
Means that your url (placed after your application root AND after your url template - index.cfm for example) would have to look exactly like this in order to match and invoke the associated method;
/hello/world
It doesn't get any more straight forward than that. Of course, if we were only to allow the configuration of these literal strings in the URL, your application would have a very difficult time being dynamic - as it's very likely to require. This is where the rest of the elements come into play nicely.

URL Variables
URL Variables allow you to construct URL's that can "vary" and yet still be matched to and invoke a specific method. This is particularly useful in the cases of strictly ReSTful URL's, where requesting information on a specific resource may need to include an object ID in the URL to make it specific such as: /user/1 OR /user/2. It simply would not make sense to have to declare the URL's /user/1 and /user/2 in the annotations themselves - as it would ultimately be impossible to account for every permutation or possible combination of both /user/ and id within the annotations themselves. This is precisely why URL variables are so important. Declaring a variable within your matching pattern is easy, simply wrap the variable name around curly-braces like so;

@url /hello/{world}
Although similar in appearance to our first example, the inclusion of the URL variable {world} in this pattern will cause it to behave much differently than an all-literal one. This specific URL pattern you've declared will match the following provided URL's;
/hello/world
/hello/developer
/hello/anyalphanumericcombinationcangohere
You'll notice that this pattern also includes the string literal "/hello/". This means that in order for a match to occur, each URL absolutely must begin with "/hello/", however, because a variable has been declared after the string, any alpha-numeric value combination can exist after "/hello/" and a match will occur. Conversely, if any other value succeeds the URL variable such as;
/hello/world/test
/hello/world/1
...Or before "/hello/"
/say/hello/world
/another/hello/world
A match will not occur.

There's another important feature URL Variable element, and that is whenever a match occurs on a URL pattern that contains variables - those variables contained in the URL will be injected into the associated method with the same name as it was configured in the annotation. It sounds more complicated than it really is. Let's take our simple /hello/{world} example. We already know that the provided URL /hello/developer will match our pattern (because the string "developer" exists in the position where the variable was declared). QS will now extrapolate the value "developer" and inject it into your associated method under the argument name "world". It's as simple as that.

Reserved Keywords {cfm} and {html}.
URL variables can have ALMOST any name. I say almost because there are only two reserved keywords that you cannot use as URL variable names and they are; {cfm} and {html}.

Before we get deeper into this, we need to diverge from the keyword subject and touch on a very important point. If you crack open the examples provided in the download, you'll notice that when navigating through them, there is a very distinct "index.cfm" file preceding our URL's. However, no such file exists. That's because QS does not need a target .cfm template in order to process it's URL's - a physical file does not need to exist. In fact, you can go ahead and change the name of "index.cfm" in any one of the sample apps and it will still work - change it to "bleh.cfm" or "yournamehere.cfm" - still works as expected. Now change it to something that does not contain the string ".cfm" - boom, you've likely gotten a strange 404 response. You probably already know this, however if you don't then this hopefully will be slightly educational. Under the covers, CF is a JEE (Java Enterprise Edition) application. It runs inside a dedicated servlet container that came as a part of the vanilla install. You can opt to install CF as a .war or .ear inside of your own JEE container - but the effect is the same. You don't need to know all of the implications of CF being a JEE application, that's the beauty of CF - it separates you from those nitty-gritty details. What this does mean though, is that in order for the servlet container to pair an HTTP request with the CF servlet - it requires the ".cfm" text to exist in the URL. Otherwise the container will not know how to service that request. This behavior can be changed as long as you have access to the web.xml file of your CF instance which we will demonstrate momentarily.

Now that you've wrapped your head around this foundational piece of knowledge, we can explain the purpose of our previously stated reserved words.

You see, QS not only allows the configuration of URL patterns that succeed the ".cfm" template, it also allows for a pattern to include both literals and variables that precede the template (e.g., index.cfm), allowing almost full control over how your URL looks and behaves. Another example is in order here;

/hello/world/index.cfm
/articles/e-commerce/how_to_enable_google_analytics_on_your_site.cfm
Are both valid URL's that QS can natively support - all you need to do is provide the correct URL matching patterns respectively;
/hello/{text}/{cfm}
/articles/{category}/{cfm}

The {cfm} keyword marks the spot in the URL matching pattern where the target template should exist. This keyword is not needed when configuring a URL pattern that only considers values located after the template - only when you are configuring a URL that assumes values prior to the template itself.

Speaking in terms of SES URL's, the most coveted URL is the one that ends in ".html". The ".html" format is a paramount indicator of static content - which search engines happen to love. QS can support this in place of {cfm} as well, in the form of {html} as you may have guessed, and still behave dynamically, but it's going to require approximately 60 seconds of configuration deep within the bowels of your CF server. If you're just that adventurous, continue with the exercise below.

First, stop your CF server and locate your web.xml file. This is located in the WEB-INF directory in your CF server root. A word of caution - it's imperative that you don't make any additional changes to web.xml other than the ones I'm about to prescribe unless you're already familiar with how to handle this file. You would be wise to make a backup of this file before you alter it. Open the file, we're going to edit. About 3/4's of the way down the file you should notice a handful of declarations that look something like this;

<servlet-mapping id="coldfusion_mapping_3">
<servlet-name>CfmServlet</servlet-name>
<url-pattern>*.cfm</url-pattern>
</servlet-mapping>
<servlet-mapping id="coldfusion_mapping_4">
<servlet-name>CFCServlet</servlet-name>
<url-pattern>*.cfc</url-pattern>
</servlet-mapping>
<servlet-mapping id="coldfusion_mapping_5">
<servlet-name>CfmServlet</servlet-name>
<url-pattern>*.cfml</url-pattern>
</servlet-mapping>

We're about to add our own now in order to support the .html extension for dynamic CF processing. While preserving the mappings that already exist, append the following to the list;

<servlet-mapping id="coldfusion_mapping_custom_1">
<servlet-name>CfmServlet</servlet-name>
<url-pattern>*.html/*</url-pattern>
</servlet-mapping>
<servlet-mapping id="coldfusion_mapping_custom_2">
<servlet-name>CfmServlet</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>

Save the file. Start-up your CF server again. You can now navigate to any one of the samples and replace the reference to ".cfm" in the URL with ".html" processing should work just as it did before. This is because now requests for .html files are being routed directly into the CF servlet. Now using the {html} reserved keyword in our URL matching patterns within the @url annotation, we can construct a pattern like this;

@url /blog/{year}/{month}/{day}/{html}
..that will allow for and match a url that looks like this;
/blog/2009/september/01/ses-and-restful-urls-are-great.html
And of course, invoke it's associated mapped controller method with a year, month, day and html argument.

This should not be considered the only way to get your application to handle .html files dynamically - there are also many other ways to accomplish this same effect - namely with URL re-writing, but we're not going to get into that in this tutorial.

There also is one caveat to this approach, and that is ALL .html requests will be routed through CF and then subsequently Quicksilver which means that it will be handled as an MVC request - not a simple file request. One possible solution to this is to simply have your actual .html files reside outside of the QS application root.

Extensions and File Formats
The final element of our dynamic URL processing feature isn't a configuration option at all - and that's the optional provision of a file format - more commonly known as an extension; .json, .xml, etc.. You do not need to account for an extension in the URL declaration, this can always be optionally passed in at the end of the URL - /hello/world.json will still match your configured URL @url /hello/{text}. Of course, there are other Quicksilver-specific behaviors that will occur when an extension is passed in - for instance, when a format of .json is request, automatic response serialization will kick in and attempt to automatically return a response - but that's another tutorial entirely.

Breakfix 0.3.70 Released

It was brought to our attention earlier this morning that there was an issue with the URL mapping mechanism in the latest build and release of Quicksilver - version 0.3.68. This has been resolved in 0.3.70 which is now available for download.

0.3.68 Released!

Our third minor release has just been finalized and made available to download on the Riaforge site. Below are the release notes for this version;

- Added improved url-pattern matching features that allow developers to configure url patterns with variables that precede the target template. E.g. /{subject}/{article}/{cfm} will match /sports/injuries_on_the_rise/index.cfm - Fixed a bug with pre-aspect processing that was not properly terminating the chain of execution when encountering a 'halted' condition. - Fixed an issue that was not properly defaulting turnTypeName and halted variables in the decorated methods attached to an object via the RuntimeObjectDecorator. - Updated samples with the tag-based hello world demo.

Lightning Fast AOP and the QS Name-Based Join Point Model

Aspect Oriented Programming is a central Quicksilver theme. In fact, it is through AOP that Quicksilver renders responses back to consuming clients to produce actual views or other forms of meaningful data streams (e.g., .json or .xml) Albeit a very powerful programming approach, it also largely up until now has been a cumbersome feature to implement depending on your language and/or framework. One of our goals for the Quicksilver framework was to make AOP both fast and easy to leverage for the average developer. This is realized with our name-based Join Point Model.

This tutorial is intended to familiarize you with the QS AOP facilities. It is also intended to be relatively brief - so we're not going to delve into all of the ins-and-outs of the AOP paradigm. If you're unfamiliar, there are many resources available however a good starting point for a summary explanation can be found with one of the usual suspects;

http://en.wikipedia.org/wiki/Aspect-oriented_programming

First off, we're going to get you familiar with the following annotations as a part of the Quicksilver API;

@aspect
@preAspects
@postAspects

Quicksilver currently supports the application of cross-cutting-concerns (aspects) at two Pointcuts (positions) - before a method is invoked, and after (pre and post respectively). The @preAspects and @postAspects annotations form the frameworks name-based Join Point Model. The @aspect annotation is used to declare an aspect. Don't worry if you're a bit confused at the moment, we'll get deeper into this as the tutorial progresses.

I'm not going to use the ubiquitous "logging" scenario that most often accompanies AOP examples - instead, I will be demonstrating a very simple "numeric ID filter".

First things first. We're going to assume that you already have an application context ready to go (with it's associate Application.cfc). If you don't - no worries, you can actually alter / add to one of the sample apps - helloworld for example.

We're going to create a simple component that does nothing more than pretend to grab an instance of some sort of User object. I say "pretend" because we're not going to create a custom User class, we're simply going to expose a method on our class who's method signature indicates it will look for a user - for the sake of simplicity. In order to see the AOP magic happening, we'll also need to annotate our method as a controller in order to have it service an HTTP request. This is not technically necessary to leverage Quicksilver AOP, that's explained later on. Below is the code for our new component;

MyComponent.cfc

// please feel free to convert this into tags if you like
component {

   public MyComponent function init() {
      return this;
   }
   
   /**
    * @url /user/{id}
    * @httpMethod GET
    */
   public String function getUser(required numeric id) {
      var string = arguments.id;
      
      return local.string;
   }

}

Just to verify that our controller is working, go ahead and navigate to your application root and provide the following path information;

{app root}/index.cfm/user/1234

If everything is set up correctly, you should actually see an exception indicating that a view can't be found;

Could not find the included template
/views/userservice/getuser.cfm.

This exception means that our method was invoked, but a corresponding view cannot be located - which of course makes sense because we haven't provided a view yet. In fact, for the purposes of this demonstration - you may or may not want to create an associated view, it depends on how adverse you are to seeing that exception.

You'll notice that our component and method is a pure implementation, free from any framework references. Our comment annotations provided the means to map the url to the method. Now, instead of providing a numeric ID for our user, pass in something non-numeric;

{app root}/index.cfm/user/asdf

You should now see an entirely different exception;

The ID argument passed to the getUser function is not of type numeric.

Again, this makes perfect sense. Our URL annotation defined the {id} url variable - which gets directly injected into the target method - in our scenario here, the value of ID is "asdf", which is obviously not numeric, so we get that error.

The purpose of the aspect that we're about to create will be to effectively "massage" the value of the "id" argument that gets passed to the "getUser()" method - to ensure it's numeric. If something non-numeric gets passed in, we're going to set the value to zero.

Time to create the aspect itself. Go ahead and add the following method with corresponding annotations to the MyComponent cfc;

/**
* @aspect idFilter
* @returnTypeName id
*/
public numeric function filterId(string id) {
   var newId = arguments.id;
   
   if (isNull(arguments.id) || !isNumeric(arguments.id)) {
      local.newId = 0;
   }
   
   return local.newId;
}

Before we delve into the details of the aspect you've just created, it's important to note that the implementation of your aspect can exist anywhere - let me say that again so it sinks in - aspects can be created ANYWHERE WITHIN YOUR APPLICATION CONTEXT. That's part of the flexibility that the name-based Join Point Model provides. I'll demonstrate it in a moment. For now let's break down what just occurred.

You'll notice again, that your aspect method is nothing more than a vanilla method. It has a very typical method signature and simple logic. There are no references to framework-specific entities at all within its implementation. This plain-old-method became a cross-cutting-concern the moment it was annotated with; @aspect idFilter.

The @aspect annotation tells the framework it can be used as an aspect, the name succeeding the annotation "idFilter" is what now can be used to create the actual point-cut. You can name your aspect anything you like - anything at all. However keep in mind that all aspects within your domain model must have unique names - otherwise an error will be thrown indicating a duplicate definition.

To complete our AOP implementation all we need to do is indicate which method requires the aspect - it's time to create our point-cut. This is done by simply annotating the "getUser()" method with the following;

@preAspects idFilter

You're done! Now, whenever "getUser()" is invoked on an instance of MyComponent the method 'filterId()' will be invoked first. The effect it will have in our application is that now, when the same "asdf" value is passed in for the user id - the "getUser()" method will be provided an id of 0 - not a string of alpha characters.

To see the results, let's take a shortcut and add the following to the middle of the "getUser()" method;

writeOutPut(arguments.id);

This will simply output to the page the value of the "id" argument. Refresh your page. If your id path information is still "asdf" (e.g., /index.cfm/user/asdf) then you should see a 0 (zero) output to the top of the page. Our aspect did it's job! It intercepted the method invocation and altered the id before it was passed into "getUser()" because it didn't meet our numeric requirement. All because you named it in the value for the @preAspects annotation - it's just that fast.

Now change the value back to "1234" (e.g., /index.cfm/user/1234). You should now see the characters "1234" output to your screen. You can now create a join-point against any method (and any number of methods) you create that happen to require a numeric argument named "id" by specifying it in the list of either @preAspects or @postAspects. Once an aspect has been created and named, it can be used all throughout your software.

@preAspects idFilter, anotherAspect, yetAnotherAspect

There's an added bonus here as well - your object is still your own after an aspect is applied. The instance of MyComponent that the framework uses is still an instance of MyComponent - not a framework-specific proxy class.

There's one more piece of this puzzle that makes it all possible, the @returnTypeName annotation. Your aspect, whether it's point-cut is before or after a method invocation, has access to each and every argument intended for the target method - including an argument which captures the name of the method being intercepted called "interceptedmethod". Your aspect can modify those arguments as needed, or add to them - the @returnTypeName annotation makes this possible. When the value for this annotation is the same as any arguments in the original argument collection, whatever is returned from the aspect method will replace the original argument value. It's a lot to chew on, but play around with it for a while. In fact, if we change the value for @returntypeName on our aspect, our functionality will no longer work as expected - because the return type will no longer replace the "id" argument. This concept is one of the Quicksilver cornerstones and is termed the Standard Framework Argument Collection and the Chain of Execution - more complicated than it sounds and I'll have other blog postings demystifying it later on.

Now, one last excersize. As I stated before, aspects can be located anywhere. You're probably looking at your MyComponent.cfc source and thinking - the filter() method feels akward here - it doesn't seem like it should be a member of that objects API. You're right, it is akward so let's move it. I'm not going to dictate to you the finer details - but go ahead and create another cfc - place it anywhere within your application context, name it anything you like. Now simply cut the "filterId()" method (with it's annotations) out of MyComponent.cfc and paste it into the new component. Ensure that the "filterId()" method no longer exists inside of MyComponent.cfc.

Now refresh your page. Nothing happened did it? That's exactly what you should expect - our aspect was still applied to the correct target method, the point-cut was preserved, it didn't matter that your software changed - drastically, it didn't matter that the method is a member of an entirely different component (hopefully one that makes more sense now). That's the flexibility and power of the name-based Join Point model. You can refactor your code, moving and renaming methods, moving and renaming components and as long as the names of your aspects declared by @aspect annotation remain the same, the Quicksilver runtime will do it's AOP job correctly.

So, when should you expect AOP behavior in Quicksilver? Whenever Quicksilver is responsible for creating the instance. This means that any method annotated as a controller (via @url) will have cross-cutting concerns applied. The same is true for any methods on auto-wired dependencies - which do not require the @url annotation. Conversely, Whenever you instantiate a new object utilizing the CF method createObject() or "new" keyword (new {Object}()) you will not get an AOP decorated instance. Of course doing this circumvents the Quicksilver runtime alltogether by using the standard CF API and will simply create an plain old vanilla instance of your component, sans any cross-cutting concerns attached to intervene.

You are now a Quicksilver AOP expert. Hopefully this has been an educational read for you. As you can see, AOP can be a very powerful instrument in your developers toolbox. Stuck or confused on any one point? Feel free to ask a question or post a comment.

Top Level Configuration

If you've had a chance to view the sample application provided in the download, then you've likely noticed how lean each applications Application.cfc file is. This is definitely by design. There are however a few optional configurations that you can provide in this file in order to control how your application initializes itself. Each has a default that the framework assumes when the config variables do not exist. Below are the details on each;

Reload config option

this.quicksilver.reload

This configuration option indicates when to reload your application context. This, of course, always occurs when your app services its first request (after the CF server is started). This config option takes the following possible values;

this.quicksilver.reload = 'autodetect'; // OR
this.quicksilver.reload = true; // OR
this.quicksilver.reload = false;

"Autodetect" is the default value QS assumes when the option is explicitly left out. With this option, QS will detect any changes to your code base and then reload itself accordingly. This is a very useful feature when developing, however along with it comes a degree of overhead - so this should never be used in a production environment.

"true" (boolean value - not a string) will ensure that your application is reloaded in its entirety on every request - regardless as to whether or not anything changed. We use this when working on the framework itself to ensure changes to core files are always up to date.

"false" (boolean value - not a string) ensures that the framework is only loaded on the initial request - no subsequent ones. Will not "look" to see if your code base has changed in any way - ideal setting for a production environment.

Context Root

this.quicksilver.contextRoot

This option indicates to the framework where your application "begins", or rather, which directory off of the web root all of your domain components are located. When ommitted, your context root is assumed to be the directory within which the self-same Application.cfc is located. However if the components that comprise your software are located somewhere else, then you can provide that information in this setting. This can be especially useful if your application context root is the same as the CF app server web root. It is likely under that scenario, that your domain classes will not also be in the web root, but under a sub-directory (e.g., /com/mydomain/). Specifying an explicit contextRoot in this scenario will ensure your application will load quickly and efficiently;

this.quicksilver.contextRoot = getDirectoryFromPath(expandPath("/com/mydomain"));

Ignored Packages

QS provides another configuration option to further customize how the app gets loaded;

this.quicksilver.ignoredPackages

This allows you to provide the fully-qualified, dot-notation package names for any directories (within the web root) that SHOULD NOT be parsed as a part of the initialization process;

this.quicksilver.ignoredPackages = "com.mydomain.config,com.mydomain.views";

More thorough documentation will be an integral part of the beta release, in the meantime we will be using the riaforge QS blog to publish helpful pieces of information as often as possible. So check back frequently.

Prefer Tags?

The advent of the CF 9 beta brought with it a much more refined and (in my humble opinion) useful version of cfscript - a very, very welcome addition. As such, most of the samples that you see as part of the Quicksilver download have been developed in the latest version of cfscript. However, those of you who prefer tags - never fear, you can write a Quicksilver supported application using CF tags as well.

Annotations however, are implemented a bit differently between the two "styles" of the CF API. As Quicksilver is an annotation-driven framework, you'll need to know the differences if you're to create an application with component / function tags. You can see in the screenshots and associated sample apps included in the downloads, annotations implemented in cfscript are done via comments prefixed with the '@' symbol (immediately preceding the entity being annotated). Annotations for tags, take on the form of custom attributes. For example;

Script Annotations

/**
* @url /user/{id}
* @httpMethod GET
*/
public User function getUser(required numeric id) {
...
}

Tag Annotations

<cffunction name="getUser" access="public" returntype="User" url="/user/{id}" httpMethod="GET">
<cfargument name="id" type="numeric" required="true" />
...
</cffunction>

The 'url' and 'httpMethod' attributes in this example, that are defined at the tag-level are effectively ignored by CF with the exception of making the information available as a part of the functions meta-data - information about the function that does not affect it's behavior. The same is true when annotations are defined at the component level;

<cfcomponent singleton="true">
...
</cfcomponent>

I've attached a new screenshot which is a tag-based version of the WorldService component - a component containing a couple of url-mapped controller methods to more clearly illustrate this, so go ahead and check it out. I will also be updating the sample apps shortly to include a tag-based version of the Hello World sample as well.

Alpha Released!

Welcome developers! The Quicksilver team is happy to announce our alpha release.

Of course, this happened in the early hours of Friday 8/14 - but I'm just now getting around to the official announcement.

We've just uploaded a newer version of the alpha release which contains a couple of small bug fixes - big thanks to some early input, and a revised set of sample applications. Also, check out the screenshots as well.

Please provide us your feedback! It's valuable to us as we want to ensure that the framework is directly meeting the application development needs of the ColdFusion community. Don't be shy - tell us about any bugs or issues by posting to the bug tracker directly on Riaforge.

Next step of course, is the beta release - on which we are actively working and will keep the community posted as to our progress.

BlogCFC was created by Raymond Camden. This blog is running version 5.5.006. | Protected by Akismet | Blog with WordPress