JS Remoting PDX-CFUG Presentation

Last Thursday I presented at the PDX CFUG on JS remoting and composite UIs.  I've uploaded my presentation and example code as a ZIP archive, and also made it available for direct preview.

Thanks to all who attended, and my apologies for having to jet out of there in such a rush, but the whole "getting a paycheck" thing is kind of important. 

Google Calendar Quick Add

When Google Calendar went public a couple days ago, I finally got to abandon my Yahoo! account for good, which doesn't pain me one little bit.  Nice interface, kind of rough in places, but it's brand friggin' new, so it's expected.  Fortunately, I was able to export/import all my events from Yahoo! directly into Google Calendar, which was nice.

Just now, I discovered the "Quick Add" feature.  It gives you a single text field to enter an event it.  Mysterious.  I tried "Golf 4pm tomorrow".  Bing: a new event named "Golf" is scheduled for tomorrow afternoon at 4pm.  How about "Easter Dinner noon sunday".  Yep, you got it.  How about "dinner with heather, next friday at 6"?  You got it, one week and one hour from now, a "dinner with heather" event. 

Talk about a sweet UI.  The rest of it's quite nice, but that, in particular, blows me away.  If you want to see usability, look no farther.

Application Modelling

For the past month or so, I've been largely immersed in modelling a totally new version of PIER.  I've done some modellling in the past, but never a soup-to-nuts model of a brand new application.  After getting most of the domain model's class diagrams fleshed out, and starting to go back over it fixing problems, I've come to a few realizations:

  1. Modelling OO systems is friggin' hard.  I'm constantly having to remember I'm modelling classes and not a DB schema.
  2. Modelling tools are really pathetic.  I've tried a pile of UML tools, and assuming I've got two grand to spend, I can't buy something that does Java 5 round-tripping and has a decent interface.  I'll admit to being spoiled by Eclipse, but even the tools that plug into Eclipse are nasty.
  3. Modelling without round-tripping is cumbersome.  There are a lot of things that are really easy to do with code that are really difficult to do with a modelling tool.  Injecting an abstract in the middle of an existing hierarchy is a good example.

All in all, however, doing a formal model is definitely a good idea, if you ask me.  I've run into a number of issues with my initial conceptions of the app that would have been very expensive to change if I'd started implementing stuff without working things through first.  We'll see how things go once the coding starts, though.  ; )

Neuromancer Update

Rob did a bunch of work with Neuromancer last night, including appying a nice template to the site, setting up a wiki (available from the site), and committing a bunch of changes that he'd made since the 0.6.0beta was released in December, but which hadn't made it into Subversion yet.  Finally, he added automated nightly builds to the site, so you can get the bleeding-edge source without having to hit the Subversion repository, and I won't have to blog patches anymore.

GTalk Enabled

I'm now GTalk enabled with my actual IM client (gaim), rather than just inside the GMail interface (which doesn't work very well).  So if you want to message me, my username is bboisvert, and I'll probably actually respond!  I can also be reached on ICQ at 4965668, AIM at se7777777n, MSN at barneyboisvert@hotmail.com, and (unfortunately) YIM at barneyboisvert.

Wheeeee!

Graph Limits Solution

After a little discussion in the comments of my previous entry that ended up with me having a long explanatory conversation with myself, I hit upon a solution that seems to be a good one.  In a nutshell, I needed to stop caring about the specific number of horizontal gridlines dividing the range, and just use what fits "best". Here's the code:

<cffunction name="getBounds" access="private" output="false" returntype="struct">
<cfargument name="low" type="numeric" required="true" />
<cfargument name="high" type="numeric" required="true" />
<cfargument name="gridSections" type="numeric" required="true" />
<cfscript>
var result = structNew();

var range = arguments.high – arguments.low;
var factor = gridSections ^ (len(int(range)) – 1);
var oneSigFig = ceiling(range / factor) * factor;

result.interval = cleanNumber(oneSigFig / arguments.gridSections);
result.low = cleanNumber(int(arguments.low / result.interval) * result.interval);
result.high = cleanNumber(ceiling(arguments.high / result.interval) * result.interval);
result.gridSections = ((result.high – result.low) / result.interval);

return result;
</cfscript>
</cffunction>

<cffunction name="cleanNumber" access="private" output="false" returntype="numeric">
<cfargument name="num" type="numeric" required="true" />

<cfset var INCLUDE_FACTOR = 100000000000 />

<cfset num = num * INCLUDE_FACTOR />
<cfreturn fix(num) / INCLUDE_FACTOR />
</cffunction>

The three parameters are the low point of the data series, the high point of the data series, and the number of grid sections you'd like.  The return values contains the bounds' low end, high end, number of grid sections, and the interval between gridlines.  Note that the returned grid sections value is not necessarily what you passed in.

The cleanNumber function is not strictly necessary, but the way I was using the values caused errors if there were "floating point arithmetic artifacts" in the results, so I wanted to clean them.  All it does is remove any decimal portion of the number that's smaller than 1/100,000,000,000.  I found that this was enough to catch any such artifacts resulting from floating point arithmetic, while being sufficiently small enough to no affect any legitimate digits.  YMMV if you've got really small numbers.

One thing that it doesn't do, however, is ensure there's "padding" around the series on the graph.  In other words, it's possible that for the high or low point of the series to end up at the maximum or minimum value of the graph.

Computing Graph Limits

If you've ever drawn a graph (manually or with a computer), you know that you have to pick upper and lower bounds for the x and y axes based on the data points you're graphing.  Usually, this is a relatively simple matter, but, at least for me, it's proven to be a difficult problem to solve in a general manner.

I've built some time-based charting components with SVG (because CFCHART sucks), and while they work very well, I simply can't find an algorithm for finding appropriate bounds that works effectively.  My first thought was to extend the range a few percent above and below the min and max, but that usually ends up with grid lines labels that are ugly (1, 1.333333, 1.6666666, 2, etc.).

I'm currently using an implementation that scales based on the number of gridlines, applied to the order of magnitude of the range boundaries.  It works pretty well, except where the order of magnitude changes close to a boundary.  I.e. if the range is from 0.01 up to 1.01, the scale will end up being from 0 to 10, which isn't desirable, because the top 89% of the chart is empty.

So with the success of my last to "fishing for feedback" posts (on BlogCFC, and tabbed documents), here's the next inline.  I'd love to hear about other people's solutions to this sort of problem, because it's quite a nasty one (or I'm an idiot and missing the obvious).

Prototype Patch

Prototype is JavaScript library, of a similar nature to Neuromancer, that I've been using of late.  It's got some really cool features that Neuromancer doesn't have (particularly when extended by Script.aculo.us), but it's not a complete replacement.  For example, no remoting support.  While working with it, I noticed that all POSTs were sent as urlencoded query strings, which works until you've got an unescaped ampersand lurking.  So I patched it to support multipart POSTs.

To use the new feature, do everything you've been doing to this point, just change your postBody parameter to be a generic object with name/value pairs representing the form fields you want to submit, rather than a query string.  The patch (for src/ajax.js) has been submitted to the Rails team under ticket #4613, and is also available below.  Note that the patch is for the source, not the assembled prototype.js, so you'll either need to get the source, patch it, and the build it (which requires Ruby), or apply the patch to prototype.js manually (which is straightforward, just have to find the right spot in the file).

Index: /home/barneyb/public_html/neuro_preso_2006_04/static/prototype/src/ajax.js
===================================================================
— /home/barneyb/public_html/neuro_preso_2006_04/static/prototype/src/ajax.js (revision 4182)
+++ /home/barneyb/public_html/neuro_preso_2006_04/static/prototype/src/ajax.js (working copy)
@@ -50,6 +50,8 @@
});

Ajax.Base = function() {};
+Ajax.Base.urlencodedContentType = 'application/x-www-form-urlencoded';
+Ajax.Base.multipartContentType = 'multipart/form-data';
Ajax.Base.prototype = {
setOptions: function(options) {
this.options = {
@@ -55,7 +57,7 @@
this.options = {
method: 'post',
asynchronous: true,
- contentType: 'application/x-www-form-urlencoded',
+ contentType: Ajax.Base.urlencodedContentType,
parameters: "
}
Object.extend(this.options, options || {});
@@ -93,6 +95,26 @@
this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;

Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+ var body = this.options.postBody ? this.options.postBody : parameters;
+
+ if (this.options.method == 'post' && typeof body != 'string') {
+ // we're POSTing and body is an object, so we need to do multipart encoding
+ var fields = body;
+ body = "";
+ var boundary = "proto" + Math.random();
+
+ for (var i in fields) {
+ body += "–" + boundary + "\nContent-Disposition: form-data;name=\"" + i + "\"\n";
+ body += "\n";
+ body += fields[i] + "\n";
+ body += "\n";
+ }
+ body += "–" + boundary + "–";
+
+ // we also have to update the Content-Type header
+ this.options.contentType = Ajax.Base.multipartContentType + "; boundary=" + boundary;
+ }

this.transport.open(this.options.method, this.url,
this.options.asynchronous);
@@ -103,8 +125,6 @@
}

this.setRequestHeaders();
-
- var body = this.options.postBody ? this.options.postBody : parameters;
this.transport.send(this.options.method == 'post' ? body : null);

} catch (e) {

Tabbed Documents

I was doing some prototyping today, and wanted to save four individual text buffers as a tabbed document.  More specifically, I wanted to save the four buffers as a single file with four sections, and when reopened, have the four sections load into four separate buffers (tabs) in the editor.

Excel has had this feature for worksheets in a spreadsheet for years, browsers have had it for a year or two, but nothing for documents.  Does that seem weird to anyone else?  Because this isn't the first time I've wanted to do something like this.

If anyone's got an editor that'll do this (and Eclipse's ability to have open editors persist across a close/open cycle doesn't count – those are separate files), I'd love to hear it.

Edit: I'm talking about a editing program here that saves files to my hard disk, not any sort of web-based thing.  I wasn't very clear on that point.  I.e. Eclipse, TextPad, Emacs, etc.

Neuromancer Again

While working on my CFUG preso for this month, I found another bug: dates weren't handled properly coming back from web service calls.  So I added support for them.  Patch file for js/io/RemoteObject.js below, or you can get the update and supporting test scripts from Subversion.

Index: RemoteObject.js
===================================================================
— RemoteObject.js (revision 10)
+++ RemoteObject.js (revision 11)
@@ -26,6 +26,7 @@
var DATATYPE_BOOLEAN = "soapenc:boolean";
var DATATYPE_NUMBER = "soapenc:double";
var DATATYPE_NUMBER2 = "xsd:double";
+var DATATYPE_DATE_TIME = "xsd:dateTime";

/**
* Variable: REMOTE_OBJECT_VERSION
@@ -85,6 +86,10 @@
{
value = parseFloat(dItem.item(z).firstChild.nodeValue);
}
+ else if (xsiType == DATATYPE_DATE_TIME)
+ {
+ value = DefaultHandler.xmlDate2JSDate(dItem.item(z).firstChild.nodeValue);
+ }
else if(xsiType == DATATYPE_MAP)
{
value = new Map();
@@ -95,7 +100,7 @@
}
}
}
- nstruct.put(key,value);
+ nstruct.put(key,value);
}
}
}
@@ -716,6 +721,10 @@
{
eval(__dfh__variable + " = parseFloat(resvalnodes.item(0).firstChild.nodeValue)");
}
+ else if (returntype == DATATYPE_DATE_TIME)
+ {
+ eval(__dfh__variable + " = DefaultHandler.xmlDate2JSDate(resvalnodes.item(0).firstChild.nodeValue)");
+ }
//}
//this is a structure (a coldfusion struct)
//else if(complextypeid != null && complextypeid.length > 1)
@@ -765,6 +774,8 @@
//show the value to stdout
if(__dfh__variable == "__neuro__myvar__")
{
+ // TODO: this throws not defined errors. Probably should be if(typeof neuro_SystemOut == "function") instead
+ // Hopefully, however, no one will ever get here, because they'll always be using a callback.
if(neuro_SystemOut != null)
{
neuro_SystemOut("\n");
@@ -774,4 +785,29 @@
neuro_Runner("");
}
}
+};
+
+DefaultHandler.xmlDate2JSDate = function __xmlDate2JSDate(xmlDate) {
+ var val = xmlDate.split("T");
+ // split it into date and time portions
+ var date = val[0];
+ var time = val[1];
+ date = date.split("-");
+ time = time.split(":");
+ // rip out the date portions
+ var year = date[0];
+ var month = date[1] – 1; // JS uses 0-11, not 1-12
+ var day = date[2];
+ // rip out the time portions
+ var hours = time[0];
+ var minutes = time[1];
+ var seconds = parseFloat(time[2]);
+ // convert fractional seconds to milliseconds
+ var millis = Math.round((seconds – Math.floor(seconds) ) * 1000);
+ seconds = Math.floor(seconds);
+ // assemble the completed date
+ var completeDate = new Date(year, month, day, hours, minutes, seconds, millis);
+ // adjust the time from UTC (Zulu) to local time
+ // TODO: change the to check for the appropriate adjustment, rather than blindly assuming UTC
+ return new Date(completeDate.getTime() – (completeDate.getTimezoneOffset() * 60 * 1000));
};