IE Caching Wastes Hours of My Life

I should have known better. I should have remembered. I've seen it before, but it bit me again: IE's abusive caching destroys the quiet peace of developing with JavaScript libraries like Prototype.

Here's the issue: I was updating the content of a <div> on a page via a form field on the same page. Very simple stuff. It worked flawlessly on Safari and Firefox (no surprises there), but failed on IE7. I tried the same code on a different machine running the exact same version of IE7 down to the xxx.xxx.xx.xxxx build, and it worked just fine. I tried it on a third machine and it failed. WTF?

The culprit, as I should have remembered from reading endless blog posts every day, was IE's overly aggressive caching strategy. Now on the Web application in question, we're setting HTTP headers to "No-Cache" to avoid these kinds of issues, and that works well for full page reloads. It does not, however, always work for XMLHttpRequest calls.

The default setting for IE is to "Automatically" check for new versions of Web pages when requests are made. If you look under "Tools" -> "Internet Options" -> "Browsing History" -> "Settings," the default is to check for new versions of Web pages "Automatically." However, IE's idea of "Automatically" doesn't take in to account XMLHttpRequests.

The solution is simple: append a random value to each AJAX request and the problem vanishes because now each URL being requested is "unique" and IE's default, aggressive, abusive caching strategy is foiled.

JavaScript example:

var cfcURL = 'myService.cfc';
var randomizer = Math.floor(Math.random()*50000);
var params = 'method=getContent&differentiator=' + differentiator;   
var myAjax = new Ajax.Request(cfcURL, {method:'get',parameters:params,onComplete:udpateSomeDiv});

Hours of my life wasted on something that should just work.

Oh, that's right, that's Apple's motto, not Microsoft's.

How Do You Stop Users from Creating Duplicate Accounts?

I'm prepping to build out a consolidated registration site for a couple of the services that we provide at my place of work. Instead of replicating a registration process across a couple systems, we're building a centralized registration system and users can then take those credentials and log in to the services that they need. (We're not replacing existing user accounts with OpenID and OAuth or anything like that — sorry!)

There's a lot of work we'll be doing to make the process simpler and more elegant than the current set of processes. One major hurdle remains, however: how to prevent users from creating duplicate accounts.

When users register for a site — any site — and if they don't access the site right away or within the next couple days, they often forget their account information. If they wait long enough, they'll even forget the email address with which they registered. This is particularly problematic in online education, as someone may take a class, then not take another for a year or two, but return to take another class at some distant point in the future when their email (or physical) address may have changed. We don't want them to make new accounts because then we don't have a unified history of their activity (and this can cause problems with things like prerequisite courses).

A common approach I've seen and we've used in the past is to check against the provided email address. If there's already an account in the system with the given email address, then you let the user know that they already have an account and provide them a link to retrieve that information.

However, if the user is trying to register with a different email address, they're going to make a duplicate account. The system isn't going to know that bsimpson@hotmail.com and santoslhalper@gmail.com are the same person. So what to do?

I'm considering the following, but am very open to suggestions:

  • Check the first and last name: if there's a match to the first and last name, suggest display a list of email addresses that match and say "Hey, if one of these is your email address, go ahead and retrieve your account info."

  • Check the first and last name and city and state or country: It's very unlikely that there are two Janet Halzipools in Buffalo, New York, but one never knows. (In parts of South India, for example, a name like Omar Khan is very common, though less so in Reno, Nevada so maybe this approach won't work particularly well.)

  • Check another unique identifier: We don't/won't/never will store Social Security Numbers, but if you have another unique identifier, you can always check against that. The University that I work for distributes unique identifiers to some, but not all, of our user base (hence the need for a separate set of authentication data). This would work pretty well, but if the user no longer has access to the email address which is associated in the system with this unique identifier, how do they retrieve their account information? Do you ask them to engage in the manual task of contacting support and then waiting for a response from support to get information updated in the system?

Once of my key considerations about any of the above strategies is this: if you find what you think is a match, how do you prompt the user to see if this is them without making them think their account security is at risk?

Again, any suggestions are welcome. Even if I can't remove duplicate account creation, if I can significantly reduce it with one or more of the above strategies, I'll be happy.

Considering Mate and Swiz

I have a confession: I don't get to do much Flex development. I read up on it. I've attended trainings. I go to classes. I watch webcasts and screencasts. I don't, however, have a lot of opportunities (or time) to build Flex apps. A lot of what I do mixes rich media and lots of text, and I'm not sure that Flex (or Flash) is the best environment for lots of text. Feel free to argue or disagree in comments below.

One thing I do know is that I don't like to work without frameworks, regardless of the language in which I'm working. Frameworks and standard libraries can take a lot of work out of developing applications, and can often help you organize your application according to best practices. In ColdFusion, I don't build applications without Mach-II (or Model-Glue, but I'm mostly a Mach-II developer).

In my Flex development experience thus far, I've been working without additional frameworks (because Flex is a framework, after all). I've played with Cairngorm and understand how applications need to be laid out using that micro-architecture. It's a bit unwieldily for me. PureMVC is powerful as well, but, again, requires all sorts of extra classes and overhead that I'm not a fan of. (And, honestly, I'm not making AS3 apps, I'm making Flex apps, so the advantages of a "framework not tied to Flex" don't really work for me.) I really like the XML-based configuration that Mach-II, Spring/ColdSpring, and other frameworks provide. I know that some people hate coding in XML, and it does have its downsides, but I think that the simplicity and clarity it brings to the process outweigh the large XML files.

As fortune would have it, there are two good options that have made their way from private alpha in to public beta: Mate and Swiz. Mate is from the team at ASFusion, who brought us some really cool Flash tips and tricks as Flash, Flash Remoting and Flex began to be integrated with ColdFusion. Swiz is the brainchild of the very smart Chris Scott, who brought us the excellent ColdSpring framework (a requirement for anyone building CF apps these days).

In my initial explorations of these frameworks, I think the strength of each is as follows:

  • Mate uses an event map to describe the flow of events/actions within the application. I really like that and it makes a ton of sense, especially coming from an XML-based framework like Mach-II.
  • Swiz uses Inversion of Control and beautiful and handy things like annotations to significantly reduce coding and elegantly manage dependencies within your application.

Mate also offers all sorts of nicely encapsulated helpers for remoting and binding and dependency injection which make it pretty attractive. I don't know enough about Swiz at this point to say if it has all the same helpers and conveniences (and honestly, the documentation for Mate helps me understand a lot more about that framework that Swiz as Swiz currently lacks an equal amount of documentation). So given those conveniences and my comfort with the XML-based configuration/"event map," I think Mate is the way for me to go, for now.

I'll blog about my experiences using Mate as I build apps with the framework. I'm personally very thankful that such smart people have provided the developer community at large with these great, powerful, time-saving tools.

Designing the Obvious Moment

Robert Hoekman Jr. has written two quite excellent books on user interface and interaction design. I read these books earlier this year, but thought I'd finally get to blogging about them as part of my ongoing book reviews.

Designing the Obvious was the first of the two books and, for me, the more successful of the two. It's rich with UI design aphorisms that simply make good common sense. Written in a simple, conversational style that draws heavily from actual Web application development experience, this book, and its counterpart just make sense. The book deserves to be read by anyone who actually builds any kind of Web application user interface, no matter how simple or complex. One of the things I really liked about his approach was the whole concept of turning beginning users into intermediate users quickly. That was the inspiration for my whole series of posts on the Contextual Guidance API. His outright demand that you make it simple, eliminate anything unnecessary in the app and in the design, and focusing on how you accomplish a task at every moment within the application is great advice. My team has taken that advice to heart and has begun designing our new applications around much of Hoekman's advice in this book. The result has been applications that visually make more sense to our clients, and clients that are really happy with what we're developing.

Designing the Moment is focused on very common, very specific moments of interaction with a Web application and how they can be made better. It's less cohesive as the previous book, which took more of a big-picture view to the art and process of designing user interfaces. In this book, the examples are very specific, somewhat less generalizable, but many of the same principles from Designing the Obvious come in to play. My sincere feeling is that a number of the solutions presented in this book will become outdated over the next couple of years (ie; blog and social networking site design) but, again, the basic principles of clarity, task-orientation and removing all but the essential are more than sound.

Designing the Obvious should be mandatory reading for all Web application developers, even those who rarely work on the UI. At the end of the day, the interface is the application, no matter what application you're using. Knowing how to create great interfaces is key to building great applications.

The Interesting and Not So Interesting of Google's Chrome

I'll readily admit that explaining the idea behind the Google Chrome project via a comic book created by the genius Scott McCloud (whose book "Understanding Comics: The Invisible Art" should be required reading for anyone who does any kind of visually creative work) is great. Licensing it via Creative Commons is even better.

But I'm not sure I get the need for another Web browser. If anything, I see this as merely a way for Google to continue to drive traffic to Google properties and bring a some "innovation" to the table in the process. If anything, it gives them an opportunity to start displaying "Works Best in Google Chrome" buttons on all their Web properties, just at the point that we've begun to collectively move away from Web applications that only work in IE or Firefox.

I'm pleased that Google has chosen to use WebKit for the rendering engine, so if a page looks fine in Safari, it'll look fine in Chrome. However, they're using their own JavaScript engine, so Chrome becomes yet another Web browser on which you must test your applications.

Looking at the features of Chrome that Google itself highlights in the overview comic book, some of their innovations just don't seem compelling enough to warrant another browser.

  • Stability: Safari, for me, is a very stable browser. Firefox and even Internet Explorer aren't half bad either. Browser crashes in this day and age are usually the result of a) out of control memory consumption by bad JavaScript, b) plug-in interaction gone haywire, or c) an error thrown at the operating system level. Chrome will address a) by having each tab be individually threaded and sandboxed, which will prevent one bad application in one tab from bringing down the browser — but that means applications can and will still crash, such as writing an email in GMail (the example used in the comic book). The plug-in interaction really can't be handled unless Chrome bans plug-ins, which is unlikely. Chrome itself is built on top of other operating systems (Windows, Mac, Linux), so it's still going to crash when there's an OS issue. No big win here, other than the individually threaded tabs.
  • A Faster JavaScript Engine: Um, SquirrelFish? Tamarin? SquirrelFish is going to be part of WebKit, and Safari 4.0, and it's mighty fast. Firefox 3.1 will contain a new, very fast JavaScript engine. Processors get faster and so JavaScript processing on those processors gets faster. Google's new JavaScript engine (V8) may be faster, and it brings to the table some interesting compile-time optimizations. It's going to generate machine code rather than run interpretatively, but I wonder if this will only work if you use Gears and the GWT. If you're not required to use Gears +/or the GWT, then Chrome still have to compile and translate the JavaScript in to machine code when the page is loaded, generating a slowdown on page load times. At the end of the day, to the end user, will it really be remarkably faster, especially as SquirrelFish and the other JavaScript engines are open-source projects with lots of smart people working on them from all over the world? If V8 turns out to be a truly excellent JS engine, then it's nice that it's going to be available for other browser makers to use. Time and performance studies will show if it's useful and adaptable for others to use.
  • Security: Sure, IE 6 is very insecure. IE 7 is better, and there are exploits available for Firefox and Safari. But those browsers are pretty dang secure compared to where we were a year or two years ago. Apple may not be the best when it comes to openness and fixing exploits on the same day they're discovered, but they're not terrible, and serious exploits are fairly uncommon. Phishing detectors are built in to Firefox and IE, though sadly not Safari, so that's nothing new. The sandboxing Chrome brings to the table is interesting, especially as plug-ins (a common source of browser crashes and Windows-based exploits) are going to exist outside and "above" the sandbox for each individual tab. I wonder, though, if plug-in makers (ie; Adobe) are going to have to rewrite their plug-ins so they work via this new kind of sandbox. Are there improvements here? Yes, in the sense of the double-sandboxing of plug-ins, and the simple sandboxing of each page so that keyloggers or other malware can't see what you're doing in a given tab. That's a nice win for consumers.
  • User Interface: Um, Safari's interface? Firefox 3's interface? How much simpler can you get with a default UI and still be able to click through basic tasks in a browser? The "nine most recent or related pages" view by default? No thanks, I'll go where I want to go.
  • Open Source: Well, WebKit, the core of Safari, is open source. The Mozilla project is open source. The final result may not be entirely open source, but how is this a big advantage or difference for Chrome?

I'll readily admit that the "Each tab gets its own thread/processing space" is rather nice. It'll make things work more smoothly. I'd imagine, however, that it wouldn't be too hard for this to be added to Safari, Firefox, or even IE. I also like the idea of the Task Manager for the browser, so you can see what resources are being consumed by each tab and kill tabs that are problematic. (Though I do find it interesting in their illustration of this in the comic book that they point to plug-ins (read: Flash) as the real problem with memory bloat and crashing Web browsers.) And Google wouldn't be able to make the Task Manager work inside of someone else's browser, so they needed to write their own to accomplish that task.

I do appreciate the fact that Google is giving a wake-up call to browser makers. I do appreciate competition. I do appreciate the different approach they're bringing to the JavaScript engine and the double-sandboxing of tabs and plug-ins.

I'm still not convinced that this is a way of driving additional traffic to Google properties (well, of course it is) and that we won't start seeing "You must use Google Chrome to access GMail. Download now!" appear in the not-too-distant future. I also hope that this doesn't hurt the development of Gears for Safari, or Firefox, as I think that's an important project and the tools it brings to the table should be available across Web browsers.

I'm also not convinced that the average user will switch away from Internet Explorer. It's the default, it's what they're used to using, it acts in a way that they are used to, and users, quite simply, hate change. Google is a trusted brand, but that doesn't mean they're going to get my parents, or my co-workers, or the students at the University where I work to change from clicking on that "e" on their desktop to "launch the Internet." If they're going to take market share away from anyone, it's going to be Firefox and Safari, and, to a far lesser extent, Microsoft's dominant Web browser.

On Creativity and Creative Culture

Ed Catmull, the cofounder and president of Pixar Animation Studios, wrote an excellent piece for the Harvard Business Review on developing a creative -- no, inspired and inspiring -- business culture. It's an excellent read and totally applicable to any number of industries, from movies or theater to software development.

Catmull's central premise -- that you must hire truly creative people and give them free reign to succeed or fail wildly, and that everyone involved in the process has value and voice and must be allowed to contribute -- speaks directly to my years of running a theater company and the thousands of hours I put in to directing. One of the very early lessons I learned is that I never had all the answers. Yes, there are lots of directors as dictatorial auteurs out there, but some of the best moments and best ideas in the productions I directed didn't come from me. They came from the set designer, or the stage manager, or the actor in the bit part, or the crew member who saw something, something inspiring and creative and right and felt they could stand up and say something to me and I, in turn, was not so stupid as to pass up their great idea because it didn't come from me. I learned you had to foster that environment from day one, and if you did, you'd get a final product that was about 100% better than a product you tried to design, develop, and deliver with autocratic power.

There are limits to this approach, of course. Someone has to take final accountability at the end of the day. Someone (or a group of someones, in Pixar's case) needs to be able to say "You know, that's interesting, but it's just not going to work." You still need leaders and teams running the show from the top, but if everyone who remotely touches the project feels that they have a voice, that their voice matters, that we are all equal and creative and working towards the same goal, you inspire passion, inspiration, and, sometimes, greatness.

That's key to building a successful team, regardless of your industry. Pixar, of course, has the advantage of money, time, reputation, and some real geniuses running the show. But the same principle applies if you're running a 68-seat theater company or building complex enterprise software. Don't discount the suggestion of the administrative assistant who uses your software or the stage crew member who has to bring on and off 38 pieces of furniture each night. They're involved. They have ideas. Listen and make them feel valued, and you'll end up with something much better than you could have ever made alone.

SOA in Practice: The Art of Distributed System Design

Thanks to the hour a day I spend riding mass transit to and from work (instead of the two hours a day I'd spend sitting in my car), I get to read a lot. I'd like to think that I'm spending most of this reading time on contemporary fiction, but that's really not the case. I'd estimate that a good 80% of my time is spent reading technology articles or books, or Entertainment Weekly. =)

Call me crazy, but I do read a good number of technical books on the subway. I've not yet delved in to the arena of books reviews, but there's no good reason for that, especially with some of the excellent books I've read of late.

"SOA in Practice: The Art of Distributed System Design," by Nicolai M. Josuttis, is a great introduction to the complexities of designing and deploying a service-oriented architecture for your business application needs. It's largely devoid of code, which some developers may find frustrating. I think that's a good thing, as SOA is about approach and not necessarily about the code it takes to set up and run those services.

I suppose you could call this a book for managers, or system architects who are just looking to get in to service-oriented architecture design, but I think it provides such a thorough, clear introduction to the idea of SOA (free from the marketing jargon of companies trying to sell you their SOA "solutions") that it's valuable for any developer who wants to create more loosely coupled services inside a single application or across applications. After reading endless issues of eWeek and Information Week and CIO Insight, I certainly knew some of the principles behind the idea, some of the SOA bus solutions available, and how governance was really important to SOA rollouts, but as for the practicalities of how one might go about doing this in the real world? Not so much.

Aside from the clear insight Josuttis brings to the book from his own SOA experiences, it's the lessons learned the hard way that I found to be most useful. Josuttis brings up a lot of issues with messaging and idempotence that I had simply not considered before and now form a cornerstone of how I'd look at developing a truly service-oriented architecture. That alone is worth the cost of admission.

Perhaps its lack of code will lead some reviewers to say this book is more for pointy-headed managers than developers (even I wanted to see a bit more on how he handled the messaging patterns he raised with some code examples, or some kind of concrete solution rather than saying "This is out there, deal with it"), but I think it's a darn fine and thorough look at getting started with SOA.

News and Tidbits on ColdFusion 9 (aka Centaur)

I just came across two blog posts which highlight some probably and some possible features for the next version of ColdFusion. My friend Adam Lehman (on Adobe's CF team) made an interesting post about the process that Adobe uses to develop new features for new versions of their products. It's a really interesting read, and the Synchronous Development process they use sounds quite interesting. You can't get the full details on that process from the SyncDev Web site because, well, their business is to sell consulting services. The key process idea of "Sell, Design, Build" (rather than "Design, Build, Sell") is a really interesting one as it ensures that your customers (and people you want as your customers) would actually want to buy your product before you ever build it. That's pretty powerful stuff for ensuring that your customer base will actually go out and buy your product. This is especially true of software, where versions above #4 tend to be about adding "nice" features when the product has already solved the core problems it was meant to solve or where the product has a free (often open-source) alternative.

While Adam doesn't talk about anything that hasn't been announced elsewhere, it's an interesting and valuable read to understand not only Adobe's product development process, but where the ColdFusion team, specifically, is coming from in determining the product's future.

Brian Rinaldi has put together a much more extensive look at all the public knowledge about ColdFusion 9. From process improvements to actual feature descriptions, his overview is a great read for anyone interested in the next version of the product and the language. I found it particularly interesting that it appears that Adobe has not gone the route of the Active Record pattern with their Hibernate integration, which I think may not save developers a ton of time but will instead allow for more flexible, robust uses of Hibernate from within ColdFusion 9. Hibernate is going to save developers a lot of time as it is, so I don't think there's a big loss in productivity by not going the Active Record route. (Implicit getters and setters will be a huge time saver, however.)

The big unknowns are the "management features," as they're sometimes called. Those are the easy-to-grasp, sexy, obvious features which make it much easier to sell a product upgrade to managers. This could include a real ColdFusion IDE, or audio/video management tools, workflow and BPEL engine integration, or who knows.

We'll know a lot more by the time MAX is over at the end of November. I'm looking forward to the time we'll finally get to play with CF9!

Doing the Upsert

In the process of developing the contextual guidance API, I tried to push as much of the work as I could to the database. I knew that I would have to do multiple queries to check and see if a user had an existing record, when their last visit was to the application, and more. I had read some time ago about the concept of the upsert, and thought this was a good time to put it in to practice.

In many applications, you need to check to see if a record exists in a table, and do an UPDATE if a record does exist, or an INSERT if one does not exist. This is a common pattern in application development. Many developers do something like this:

Query the table to see if a record exists which matches the passed ID
IF match found {
   Run an UPDATE query
} else {
   Run an INSERT query
}

That's fine, but ultimately requires two trips to the database, or more, depending on what other conditional processing you need to do. The upsert combines all of this in to a single SQL statement. In addition, I needed to run some other database-related logic that would affect the SQL statement. If a user hadn't visited the application in more than 180 days (6 months), I wanted to treat them as a new user and delete their previous record in the log table. (You could set an "isDeleted" flag or delete the record, that's up to you.)

My solution is as follows, and combines some conditional logic in ColdFusion with conditional logic in the SQL as well. This is for MS SQL Server, but the concept applies to pretty much any database.

<!--- First check and see if a record for this user has been updated in the last 30 minutes. If so, don't do anything as that's not really a new session. --->
DECLARE @lastVisitDate datetime
set @lastVisitDate =
   (
   SELECT lastVisit
   FROM logTable
   WHERE userID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.userID#" />
   AND appName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationName#" />
   <cfif Len(arguments.applicationSection)>
      AND sectionName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationSection#" />
   <cfelse>
      AND sectionName IS NULL
   </cfif>
   AND DateDiff(n, lastVisit, getDate()) > 30
   )
         
<!--- If the @lastVisitDate value is not null, a record was found matching the criteria, so we can do an update or insert, as needed. --->
IF @lastVisitDate IS NOT NULL
   BEGIN
      <!--- If the user hasn't visited the app in more than 180 days, treat them as a brand-new user by deleting their record and starting fresh. --->
      IF DateDiff(d, @lastVisitDate, getDate()) > 180
       BEGIN
            DELETE FROM logTable
            WHERE userID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.userID#" />
            AND appName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationName#" />
            <cfif Len(arguments.applicationSection)>
               AND sectionName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationSection#" />
            <cfelse>
               AND sectionName IS NULL
            </cfif>
       END
               
<!--- Try the update first. If there's nothing to update, we need to do an insert instead. --->
      UPDATE contextualGuidanceLog
      SET totalVisits = totalVisits + 1,
         lastVisit = <cfqueryparam cfsqltype="cf_sql_timestamp" value="#CreateODBCDateTime(Now())#" />,
       previousLastVisit = (
                  SELECT lastVisit FROM contextualGuidanceLog
                  WHERE userID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.userID#" />
                  AND appName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationName#" />
                  <cfif Len(arguments.applicationSection)>
                     AND sectionName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationSection#" />
                  <cfelse>
                     AND sectionName IS NULL
                  </cfif>
                  )
         WHERE userID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.userID#" />
       AND appName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationName#" />
         <cfif Len(arguments.applicationSection)>
            AND sectionName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationSection#" />
         <cfelse>
            AND sectionName IS NULL
         </cfif>
               
      <!--- If nothing was updated, then the resulting @@rowcount value will be zero. We need to do an insert in this case. --->
      IF @@rowcount = 0
         BEGIN
            INSERT INTO contextualGuidanceLog (
               userID,
               appName
               <cfif Len(arguments.applicationSection)>, sectionName</cfif>
               )
               <!--- totalVisits, lastVisit and previousLastVisit have defaults created in the table by the db --->
               VALUES (
                  <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.userID#" />,
                  <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationName#" />
                  <cfif Len(arguments.applicationSection)>
                        , <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.applicationSection#" />
                  </cfif>
               )
       END
   END <!--- End the "IF @lastVisitDate IS NOT NULL" IF --->

This may look a bit complex, but the key thing here is the UPDATE followed by the IF @@rowcount = 0 statement. If no records were updated (meaning there was no record to match the passed values), then we do an insert. This is just like our original logic, except we're doing everything in one query, rather than two. Because I also needed to check the last time a record for this person was updated and make sure we're not updating it more than once every 30 minutes, I combined 3 potential <cfquery> calls in to a single one.

There's one issue, though, that I'm not too happy with:

I realize that I'm repeating a bunch of code to generate the WHERE clause in these SELECT statements. I can't generate this text dynamically as a string and then <cfoutput> the WHERE clause because I'm using <cfqueryparam>, as I damn well better be. I guess I could build a dynamic string and then wrap it in the Evaluate() function, but that seems pretty ugly to me (and may not work because of how CF's engine parses the query text and does the binding to the values in a <cfqueryparam> tag). Though I guess using Evaluate() isn't any uglier than the repeated code for the WHERE statement.

I like the upsert approach. I'll be using it from here on out in these situations. If anyone has any suggestions about the repeated WHERE clause code, I'd love to hear them!

Wrap Up on the Contextual Guidance API

Given all the background thought I've done on the contextual guidance API, it was a breeze to implement. Planning before coding really does pay off!

The table I'm using to store the data for the tool looks like this:

As you can see, I've simplified things and haven't gone the route of more advanced, multivariate analysis on usage patterns for a users within an application. I certainly could have, but given that determining a user's experience level with an application is an act of generalization and not a precise science, this seemed right. Or maybe I'm lazy, or like things simple, or both.

In addition to a totalVisits value, I'm storing two date values per user, per application, per section of the application (if that's passed). This allows me to place some context on the user's activity within the application. Instead of just saying "oh, well, they haven't visited this application in 30 days, they should be given the beginner materials again," I can instead say "Well if they haven't visited in more than 30 days, but less than 90, and their total visits were greater than 50, give them the intermediate materials because they probably remember some of what's going on here." Again, in an ideal setup, I should look at not only the number of visits (or visits per section), but average visits over periods of time, the length of breaks between visits, and the exact actions they took within the application to provide them with highly specific contextual guidance. I think that for my users and my applications, being able to look at their activity within sections of a complex application given a rudimentary temporal context will be just fine. If anything, this simpler version of the logic will result in a more consistent display of help/guidance as the user moves from beginner to intermediate user to expert. When you try to get super-specific about guessing what a user is trying to do and provide help each step of the way, you are in danger of ending up with Clippy.

There will be one more related post about the API. It has to do with the magic of upserts and how I thought moving some business logic in to the query itself would save me code, but may not have.

More Entries

BlogCFC was created by Raymond Camden. This blog is running version 5.8.001.