OO and CFForm

Maybe I'm taking this too literally... as often happens with people who have Asperger Syndrome. Anyway, the other day someone posted a comment on Dan Vega's blog about CF and OO. Here's the comment:

I will feel more comfortable calling CF OOP once it's not as tag based. Once i can create and extend some of the built in tags. When i don't have to put on my page i could simply write a component that can create an instance of the cfform and append other objects and then call a construtor method on the CFform Object to display it on the page. Cough cough ".NET"

I hope i don't get called a MS fanboy!

-- Aaron

I have mixed emotions about this comment. On the one hand, sure it might be nice to be able to extend some of CF's built in tags to improve the existing functionality? On the other hand, it's almost never really necessary... Aside from the ability to create all manner of "wrappers" for CF's native features (which I've done a lot of over the years), you can certainly use Java reflection for anything that can't be done with the native CF tags. And for that matter, the Java Loader makes doing that even easier.

But then why even bother with CFForm as an example? In my mind CFForm is kind of a quick way to get things done if a) you're new to CF and don't have a better form framework handy or b) if you're trying to do something for yourself quickly and quality isn't a concern. For that matter there are a bunch of open source or alternative form frameworks around. Aside from the form tools in the onTap framework, cfUniForm and coreForms both on RIAForge, New Zealand native and a good friend of mine, Matt Walker produces one called TerraForm.

Around the time that Flash forms and XForms were introduced in ColdFusion, Matt and I were talking about his project, wondering if the new features in ColdFusion would make TerraForm obsolete. It did not. And these days the general recommendation for Flash forms even is "use Flash" or build them in Flex. For that matter, the form tools in the onTap framework as well are still far more powerful than CFForm (and no harder to use). To be honest, the XForms implementation did inspire some new features, but it's still no replacement for the onTap framework's templating engine, (which imo frequently resembles that "code behind" concept you see in some of the newer MS technologies that Aaron mentioned).

But what tends to stick in my craw about these kinds of comments is the insinuation that the SYNTAX of the language has anything at all to do with its being or not being OO. So cfform is tag based? So what?! There are better form tools available anyway. But for that matter, even with the currently available features in ColdFusion 8, you can still implement CFForm in CFScript. Yes! Yes you can!

How does this look as a simple starting point?

<cfscript>
   myForm = CreateObject("component","form").init();
   
   myForm.add("productName",true,"Product Name").add("productPrice",true,"Price");
   myForm.add(name="productDescription",type="textarea",label="Description");
   
   writeoutput(myForm.render())
</cfscript>

Now I'm not going to be using this code myself, largely because I already have a set of form tools that are far more flexible already. This does however show that you can indeed even work with cfform in script. For the full code, you should see a download in the links below.

Nightly Build

In the process of working on the Galleon ports I noticed that the sql library's subqueries feature wasn't working in update statements, so I resolved that and while I was at it, I added character masking to the form tools numeric validation so that when javascript is enabled, users won't be able to enter non-numeric characters into text fields that have numeric validation.

Here's a sample of how to use the sql update:

<cfscript>
ds = request.tapi.getObject("datasource").init();

ins = { hitcount = ds.getStatement("select").init("mytable","hitcount + 1") };
ins.hitcount.filter("primarykey","keyvalue","=");
update = ds.getStatement("update").init("mytable",ins);

update.setFirstFilter(ins.hitcount.getFirstFilter());

update.execute();
</cfscript>

I declared ins using ColdFusion 8's implicit structure syntax for convenience sake, since the update statement is initialized with the name of the table and a structure with the values you want to insert into the record. That 2nd to last line reuses the filter object from the select statement so that both the select and the update have the same filters - plus not having to instantiate another object is more efficient.

Date Picker

The new build uploaded tonight integrates the DynArch calendar widget for date selection with input elements.

Learning Curves

So I was reading the documentation for Spry and realizing that the folks on the Dreamweaver Team intentionally avoided making the form validation widgets similar to the onTap framework's form validation tools (like I mentioned before) because part of their design philosophy behind Spry involved not making people learn a new tag library. According to the documentation, this makes Spry easier to learn, because it means users will be relying on skills they already have with HTML, CSS and JavaScript.

Okay, sounds great!

Here's the catch. They're assuming that this:

<input type="text" name="myTextField" id="myTextFieldID" />

...

<script language="javascript">
   ...
   myText = Spry.Widget.ValidationTextField("myTextFieldID", "integer", { useCharacterMasking:true, minValue:"0"});
   ...
</script>

Is somehow easier to grasp and requires a smaller learning curve than this:

<input type="text" name="myTextField"
spry:validate="integer"
spry:useCharacterMasking="true"
spry:minValue="0" />

I don't know about you, but I find it hard to imagine how the eminently legible second example would be harder to grasp and have a bigger learning curve than the former, merely because the person implementing it already knows JavaScript. Aside from the fact that they've merely arbitrarily asserted that "it's easier to learn something in JavaScript than it is to learn essentially the same thing in XML", the example from actual Spry code actually involves at least one additional opportunity for confusion and one additional coupling point.

The additional opportunity for confusion is that the text input now requires an ID attribute which wasn't necessary if you used namespace attributes in the input element, because the XML version implies that the current XHTML node (the input element) is the item being validated (i.e. it's encapsulated).

The additional coupling point is that the javascript below has to know in advance (from the programmer) what type of input element is being validated. So now you've got to specify that this particular input element is a "textfield" in two places, plus you've got to make sure the textfield widget and CSS are loaded before you create the validator. Yay for high coupling! What happens if you comment out the script tag that loads the library? Well in the XML example, if it's architected properly, the only thing that happens is the page ignores the spry attributes. In the Spry example, you get javascript errors. Or perhaps more likely, if you comment out the input element in the XML example, you're done. If you comment out the input element in the Spry example, you're forced to separately comment out the line of JavaScript that creates its validation widget.

The XML example would also actually offer the opportunity for some interesting alternatives -- individual widgets could in theory be loaded via a WebService (or at least another server) instead of requiring that they be loaded inline from your own application. Attributes on the individual nodes could be modified to change the validation at run-time without having to dig into the guts of the validation widget, etc.

I'm being a bit scathing here I know. And I realize that the developers on the Dreamweaver team will probably not enjoy reading these comments (if they ever do). I'm just venting a few frustrations and trying to help people understand better design at the same time. The Dreamweaver team's objective of creating a system with very little learning curve is laudable. Their intent is admirable. Their execution however is far from achieving their stated goal. IMO in the present tense, spry = poor encapsulation + big learning curve...

Small Select Bug Fix

I'm reminded that when ColdFusion 7 was released, it shipped with a small bug in the XForms implementation which caused multiple-select input elements to display with none selected if a user had previously selected multiple options. It had turned out to be something fairly simple, an oversight in their default XSL sheet and at the time I'd provided the fix for Jeff Small (although at the moment I can't find his blog entry where he posted it for everyone else -- send me the url if you have it and I'll post it here). I'm sure it was something the folks on the CF development team just hadn't antiscipated, after all how often do you use a multiple-select? (Of course, I say this expecting a quick response from the couple of folks who use them frequently.) :)

It's sort of come back to haunt me... In the previous versions of the onTap framework, I'd built in query-driven select input elements. The first iteration of these were unfortunately painfully slow. The problem at the time was that I was making it create a whole new html element structure for each option, instead of simply holding on to the query until display time. It turned out to be far too much overhead, and just as well most of the data in those structures was going unused anyway. So I then moved to the more logical approach of storing the query in the element structure for the select input and looping over them to create the options at time of display. This worked pretty well and even allowed me an easier way of fetching the current value of the select input by using the native ValueList() function. However, when setting the current value, I was still looping over each record in the query, like this:

<cfloop query="input.query">
<!--- get the value of the current option --->
<cfset thisvalue = input.query.inputvalue[currentrow] />
<!--- check to see if it's selected --->
<cfset isselected = ListFindNoCase(input.selected,thisvalue) />
<!--- reset the option selection --->
<cfset input.query.selected[currentrow] = iif(isselected,"thisvalue",de("")) />
</cfloop>

You can see from this code how I can then use ValueList(input.query.inputvalue) to fetch the list of currently selected options. Well this was great, and then shortly after I started working on version 3.0 of the framework I realized that there may be an even more efficient way. Instead of looping over the query one record at a time, I could set the entire query all at once and it could potentially be much more efficient. Something like this:

<!--- reset all the options --->
<cfset ArraySet(input.query["selected"],"",1,input.query.recordcount) />
<!--- get the indexes of the selected options --->
<cfset selected = getSelectedIndexes(input.query,input.selected) />
<!--- set the selected flag for selected options --->
<cfloop index="i" list="#selected#">
<cfset input.query.selected[i] = input.query.inputvalue[i] />
</cfloop>

So in this case the loop only processes one iteration for each selected option, instead of for each record in the query. In some cases where there are a lot of options in the list this can be much more efficient. I tested this after I implemented it and it is much more efficient when the select list is excessively large: hundreds of options or more, which may not be the best idea in the first place, but that's another blog. For most uses though, the efficiency probably won't change much from the last version.

The problem lies in that getSelectedIndexes() function (which is a simplification for the sake of the blog -- suffice to say that it creates a list of query indexes that should be selected without looping over the query). The algorithm I had for determining which query indexes were selected turned out to have a minor flaw in it and so where I had a query containing the values "en,en_CA,en_US" (and a few others), it was selecting all options beginning with "en", so the options for CA and US were being additionally selected when only "en" should have been. Of course I fixed this yesterday and uploaded a new build of the framework core code a few minutes ago.

I will give one caveat and that's the form tools do assume that you won't have any linefeed characters (ASCII char(10)) in your option values. So if for some reason you need that particular character in your data, you'll have to replace them with something else before displaying the form and after submitting it. Probably most people won't have any issues with that, since the inputvalue column will usually contain either integers, UUID strings or some other equivalent. Speaking of which, this is probably why I didn't catch this sooner, because I tend not to use autonumber / identity columns (though they're better supported now), and since most of my select elements contain a UUID equivalent, the values generally don't offer that kind of overlap.

Hickory Dickory

For a long time now the onTap framework has included features for managing timezones, something for which ColdFusion's support is traditionally somewhat weak. But timezones are a messy issue and notorious for creating unexpected challenges in software development. For example, it's generally considered a best practice to store dates in UTC (formerly GMT) timezone because that time zone is neutral, having no offest and no daylight savings adjustments, meaning that 5-o-clock is 5-o-clock is 5-o-clock. Really what this means is that it's a good baseline from which times for other timezones can be extrapolated.

That's actually the intention of UTC in the first place and why it is that many platforms like ColdFusion and SQL Server have native functions for getting the current time in UTC or converting times to and from UTC. SQL Server has a GetUTCDate() function for example although I'm not aware that it has any native functions for converting to or from. So the onTap framework by default will assume that you want to store dates in UTC and when you enter dates into forms, if you've added a time, it will convert that date/time value to UTC before it's stored. If there's no time, it leaves the date alone.

(Of course if you don't like the fact that the framework handles this time-zone converting for you and you'd rather do it all yourself, you can disable it by setting the timezone for the request to UTC. If there's no offset and no DST adjustment for the current timezone, then it will only be adding or subtracting zeros from your dates, effectively canceling out the feature.)

A while back I implemented a feature in the SQL Abstraction tools that allows you to use "now" as an alias for the current time in UTC when performing inserts or updates. I did that because I was finding that I wanted to insert the current UTC time frequently (you might say routinely) and at the time it was a bit of a challenge because the tools' default behaviors were converting the date from the current timezone of the request (which belongs either to the user or to the server). So in order to get the "now" UTC I was having to first convert the UTC date to the local time zone so that it would be the current time in UTC when it was converted back. So the fix that made it much easier to handle that common task was allowing me to assign it via "now", i.e. myobject.update(SomeDateColumn="now"). That's pseudocode -- the real thing is a bit more involved but you get the idea.

I was just noticing a similar issue with the forms this evening. It wasn't quite as simple as I would like to create a date input in a form with a default value of the current time (or date). So I decided to fix that by adding a feature to the input validator that will interpret values of "now" or "today" as the current date, with or without the time respectively. So in my case, the input element is

<input type="text" name="historydate" tap:default="now" tap:validate="date" />

And that gives the input element a default value of today's date plus time. If everything is working properly, it should match the clock in my system tray. :) It's important that the input be validated as a date, otherwise the framework doesn't even try any date massaging. They also get massaged a second time when you use the framework tools to validate the form on the server side, so it's important also make sure you are performing the server-side validation unless you're doing your own date massaging. On the other hand if you use tap:dbtable in your form to automate validation for the form based on your database metadata, then you won't need to include the tap:validate attribute you see in this snippet since the automation will add that for you.

Part of the upshot of this is that if javascript is disabled in their browser, users might be able to type "now" or "today" into the form to get those values. (If they have javascript enabled, the default javascript validation will stop them, although if you really like the idea of using "now" and "today" in forms, you can change the regular expression the framework uses for javascript validation.

Oddly, while I was in there adding this, I noticed also that the localization directories were being included in the wrong order... So /_application/l10n/en/us/100_dateformat.cfm that overwrites the date format for the united states to be mm/dd/yyyy was being included before instead of after /_application/100_i18n.cfm where the default format of dd/mm/yyyy for the rest of the world is set. Basically it wasn't using US date formatting in the US. So I fixed that. It seems odd that I hadn't seen that earlier, since I know it was working before. Anyway it's fixed now (and for most folks wouldn't have been a deal-breaker anyway, since most of us in the US could just set the value application-wide to use US dates).

p.s. I updated the zip archive as usual.

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