The google-gears group gets a lot of questions from .NET developers. So I decided to help out.
In this tutorial, we will build a simple ASP.NET timesheet entry application (because, you know, everybody loooves timesheet entry). Then, we'll bolt on Google Gears to eliminate any excuses for not entering timesheets. Say, you're sitting 15,000 feet above ground in cushy herd-class accommodations, with your laptop cracked about 60 degrees, keyboard pressed firmly against your chest, praying that the guy in front of you doesn't recline.
This is the point where Google Gears comes to save you from yet another round of Minesweeper. You fire up your browser, point it to your corporate Intranet's timesheet entry page and — boom! — it comes right up. You enter the data, click submit and — boom! — the page takes it, prompting your aisle-mate to restart the search for the Ethernet socket in and around his tiny personal space.
And after you cough up 10 bucks for the internet access at the swanky motel later that night, your browser re-syncs the timesheets, uploading the data entered offline to the server. Now, that's what I call impressive. You can read more about features and benefits of Gears on their site later. Right now, we have work to do (if you'd like, you can download the entire project and follow along).
First, we start with the ASP.NET part of the application, which is largely drag-n-drop:
It is probably worth mentioning that no amount of styling will fix some obvious usability problems with the data entry in this particular piece of user interface, but hey, I didn't call this article ASP.NET on Gears: A Production-ready Application, right?
Moving on to Gears stuff. This part of the show contains graphic hand-coding and conceptual thinking that may not be appropriate for those who build their stuff using the Toolbox bar and Design View. People who are allergic to Javascript should ask their doctor before taking this product. Just kidding! You'll love it, you'll see ... I think.
For this tutorial, I chose to use the 0.2.2.0 build of Gears, which is not yet a production build, but from what I heard will be shortly. This build offers a quite a bit more functionality for workers, such as HttpRequest and Timer modules, and as you'll see shortly, we'll need them in this application.
Let's first figure out how this thing will work. When connected (online), the application should behave as if Gears weren't bolted on: entry submissions go directly to the server. Once the connection is severed (application goes offline), we can use LocalServer to serve application resources so that the page still comes up.
Obviously, at this point we should intercept form submission to prevent the application from performing a POST request (those are always passed through the LocalServer). As we intercept the submission, we put the submitted data into a Database table.
Then, when back online, we replay the submissions back to the server asynchronously, using WorkerPool and HttpRequest, reading from the Database table.
Speaking of back online, we'll need some way to detect the state of the application. We'll do this by setting up a WorkerPool worker, making periodic HttpRequest calls to a URL that's not registered with the LocalServer. When request fails, we deem the state to be offline. When request succeeds, we presume that things are online. Simple enough?
To keep our dear user aware of what's going on, we'll need to do quite a bit of DOM manipulation. No, not that Dom. This DOM. For instance, the data, entered offline should be displayed for the user in a separate, clearly labeled table. We will also need to know of events like the user attempting to submit the form, so that we could intercept the submission and stuff it into Database.
Oh, and there's one more thing. Since we build this application to operate both offline and online, we can't rely on server-based validation. For this task, I chose to write my own client-side validation, but you can try and tinker with the standard ASP.NET 2.0 validation controls and the crud they inject in your document.
To summarize, we need the following components (let's go ahead and name them, because naming things is fun):
Piece of cake! The only thing left is writing some code. Perhaps we should start with defining how these pieces of the puzzle will interact. To keep code digestible and easy to hack on (it's a tutorial, right?), we will make sure that these interactions are clearly defined. To do that, let's agree on a couple of rules:
It's like an old breadboard from your science club tinkering days: each component is embedded into a block of non-conductive resin, with only inputs and outputs exposed. You plug the components into a breadboard and build the product by wiring those inputs and outputs (figure 8).
In our case, since our components are Javascript objects, we'll define an input as any Javascript object member, and an output as an onsomethinghappened handler, typical for DOM0 interfaces. And here we go, starting with the Database object, in the order of the alphabet:
// encapsulates working with Gears Database module
// model
function Database() {
// removes all entries from the model
this.clear = function() {}
// opens and initializes the model
// returns : Boolean, true if successful, false otherwise
this.open = function() {}
// reads entries and writes them into the supplied writer object
// the writer object must have three methods:
// open() -- called before reading begins
// write(r, i, nextCallback) -- write entry, where:
// r : Array of entry fields
// i : Number current entry index (0-based)
// nextCallback : callback function, which must be called
// after the entry is written
// close() -- called after reading has completed
this.readEntries = function(writer) {}
// writes new entry
// params : Array of entry fields (StartDateTime, DurationMins,
// Project, Billable, Comment, FormData)
this.writeEntry = function(params) {}
}
It's worth noting that the readEntries method mimics the archetypical Writer and asynchronous call patterns from the .NET framework. I hope you'll think of them as the familiar faces in this crowd.
The DOM component has the most ins and outs, primarily because, well, we do a lot of things with the browser DOM:
// encapsulates DOM manipulation and events
// view
function DOM() {
// called when the browser DOM is ready to be worked with
this.onready = function() {}
// called when one of the inputs changes. Sends as parameters:
// type : String, type of the input
// value : String, value of the input
this.oninputchange = function(type, value) {}
// called when the form is submitted.
// if it returns Boolean : false, the submission is cancelled
// submission proceeds, otherwise
this.onsubmit = function() {}
// hooks up DOM event handlers
this.init = function() {}
// loads (or reloads) entries, entered offline by creating
// and populating a table just above the regular timesheets table
// has the same signature as the writer parameter of the
// Database.readEntries(writer)... because that's what it's being
// used by
this.offlineTableWriter = {
open: function() {},
write: function(r, i, nextCallback) {},
close: function() {}
}
// provides capability to show an error or info message. Takes:
// type : String, either 'error' or 'info' to indicate the type of
// the message
// text : String, text of the message message
this.indicate = function(type, text) {}
// grabs relevant input values from the form inputs
// returns : Array of parameters, coincidentally in exactly the
// format that Database.writeEntry needs
this.collectFieldValues = function() {}
// returns : String, URL that is set in of the form action attribute
this.getPostbackUrl = function() {}
// removes a row from the offline table. Takes:
// id : String, id of the entry
this.removeRow = function(id) {}
// remove the entire offline table
this.removeOfflineTable = function() {}
// enable or disable submit. Takes:
// enable : Boolean, true to enable submit button, false to disable
this.setSubmitEnabled = function(enable) {}
// iterate through fields and initialize field values, according to type
// Takes:
// action : Function, which is given:
// type : String, the type of the input
// and expected to return : String, a good initial value
this.initFields = function(action) {}
}
Monitor has a rather simple interface: start me and I'll tell you when the connection changes:
// provides connection monitoring
// controller
function Monitor() {
// triggered when connection changes
// sends as parameter:
// online : Boolean, true if connection became available,
// false if connection is broken
this.onconnectionchange = function(online) {};
// starts the monitoring
this.start = function() {}
}
Is this a simplicity competition? Because then Store takes the prize:
// encapsulates dealing with LocalServer
// model
function Store() {
// opens store and captures application assets if not captured already
// returns : Boolean, true if LocalServer and ResourceStore
// instance are successfully created, false otherwise
this.open = function() {}
// forces refresh of the ResourceStore
this.refresh = function() {}
}
Synchronization algorithm in this tutorial is exceedingly simple, we basically just start it and wait for it to complete. As each entry is uploaded, the Sync component reports it, so that we could adjust our presentation accordingly:
// synchronizes (in a very primitive way) any entries collected offline
// with the database on the server by replaying form submissions
function Sync() {
// called when a synchronization error has occured. Sends:
// message : String, the message of the error
this.onerror = function(message) {}
// called when the synchronization is complete.
this.oncomplete = function() {}
// called when an entry was uploaded to the server. Sends:
// id : String, the rowid of the entry
this.onentryuploaded = function(id) {}
// starts synchronization. Takes:
// url : String, the url to which to replay POST requests
this.start = function(url) {}
}
Finally, the Validator. It's responsible both for providing good initial values for the form, as well as making sure the user is entering something legible.
// encapsulates validation of values by type
function Validator() {
// provides good initial value, given a type. Takes:
// type : String, the type of the input, like 'datetime' or
// 'number'
// returns : String, initial value
this.seedGoodValue = function(type) {}
// validates a value of a specified type. Takes:
// type : String, the type of the input.
// value : String, value to validate
// returns : Boolean, true if value is valid, false otherwise
this.isValid = function(type, value) {}
}
Whew! Are we there yet? Almost.
This is where we pull up our sleeves and get to work. There's probably no reason to offer a play-by-play on the actual process of coding, but here are a couple of things worth mentioning:
Armed with these Gear-ly pearls of wisdom, you jump fearlessly on the interfaces above and get coding. Or... you can just see how I've done it (listing 3).
Feed the monkey? Wha... ?! Just wondering if you're still paying attention. Technically, we're done here. The application is working (to see for yourself, download the screencast or watch it all fuzzy on YouTube).
As you may have gleaned from our coding adventure, Google Gears offers opportunities that weren't available to front-end developers before: to build Web applications that work offline or with occasionally-available connection, to add real multi-threading to Javascript, and much more. What's cool is that Gears are already available on many platforms and browsers (including Internet Explorer), and the list is growing quickly. Perhaps PC World is onto something, calling it the most innovative product of 2007.
But don't listen to me: I am a confessed Gearhead. Try it for yourself.