Thursday, 8 March 2012

Who needs objects?

Dave Thomas has said that the object abstraction is too complex for the majority of programmers. Most business software is CRUD with a bit of business logic mixed in. And it can scale by building loosely coupled systems (works for the internet, eh). Dave is a giant; he sees far. I think he's right.

So what does this mean to an object evangelist like me?  Probably not much. That vacant look on most people's faces when you try to explain objects says it all. If it does not address an immediate need, object abstraction is noise.

At the Toronto Smalltalk User Group meetings we sometimes have one or two students from Ryerson University. By attending they've already indicated that they're interested in more than the generic C syntax procedural stuff they learn in school. Joshua Panar and Dave Mason, the two profs that sponsor our group and use Smalltalk in their OO course, have said that getting the regular students out is a challenge. They're not interested. They don't see it as improving their education or job prospects. Suggesting that they should broadening their horizons falls on deaf ears.

There are two types of programmers: the toolsmiths (abstractionists) and the tool users (constructionists). Smalltalk developers seem to all be abstractionists. It is natural of us to extending our environment. Want a framework? Build it. Need a new compiler behavior? Add it. It's easy; it's common for us, yet unheard of by others.

Most programmers are constructionists. They have a job building and maintaining business applications. As Smalltalkers we ask ourselves: how can we get these programmers to use Smalltalk, to see how much more productive and enjoyable our environment is? The answer, I believe, is to reduce barriers to entry.

How to do that? Here are my wishful thinking answers...

  • Merge the dialects (ya, I know: unlikely). Selecting a dialect as the first step in exploring Smalltalk is a big problem. You need to know a lot to make a good decision, at the point where you know little. Yes, the VW & Digitalk merger was a bust. But that was another time. But I can dream...
  • Use a common online forum.  The Balkanization of the Smalltalk community is a problem. Think of how hard it is for a Smalltalk curious person to find information. If we at least used a common forum, like Stack Overflow, it would be easier to find cross dialect posts, and it would be more visible to the larger developer community. I'll advocate for it again at the the upcoming STIC conference, but I must be turning into a cynical cranky old fart, because I don't think there will be a change.
  • Support simple scripting. I know it's been done in various ways (S# was cool), but we should be able to point to a simple script tool for people to try. If there is a good option out there, consider this: I'm a Smalltalk cognoscenti, and I'm not aware of an option that does not require firing up an image. What does that say about how well we get the word out?
  • Start with prototype objects. Self and javascript got it right. It is easier to explain objects if you can defer talking about classes, and where the value of classes is discovered as a useful pattern.
  • Make Smalltalk IDEs rock. I know the Smalltalk vendors and volunteers have done a great job with the resources available, but VisualStudios and Eclipses of the world are slick by comparison.
  • Examples. Lots of examples. It would be great if we could point to real applications that people could fire up, test and explore. And templates; wizard driven templates to help build new applications, like those found in MS Access. Need an application to track students? Here's an example and / or a tool to help you get started. If nothing else, make it easy to get started.

Yes, abstractions are hard. But abstraction allows you to do things that would be far too difficult and expensive otherwise. Knowing how to think in abstract terms is a powerful skill that will make you a better technologist. It is our job, as those that understand this, to make it self evident to others.

Simple things should be simple. Complex things should be possible.

Tuesday, 31 January 2012

VW Code Coverage

The past few days I've been working with the code coverage extensions to SUnitToo in VW (SUnitToo(verage) and SUnitToo(lsoverage). It is an impressive tool. And the metrics accurately reflected the way I developed some packages. Our in-house issue tracker and patch manager had tests added after the fact, with a resulting code coverage of 30%. The Report4PDF package, the PDF4Smalltalk based report writer I'm working on, was developed from the start with SUnit tests, with a code coverage of 65%.

At first I thought that 65% seemed a bit low, since all the code has associated tests. But the code coverage is smart: it does not just measure method hits, but which paths within a method are exercised. In many cases these were exception and error message branches; the paths less taken. But some cases should have been tested. These were very narrow special cases that I had simply not thought of. The code looks OK, and I'm confident that it will work, but I won't release it without an SUnit test.

If feels like I've been given a flashlight to see the dark nooks and crannies of my code.

I also like the idea of using the path count (the denominator in the code coverage number) as an application complexity metric. It could work well when used with number of classes, number of methods, and average method size. I wonder if 'number of paths per method' would be useful.

It is an impressive tool. Thanks to all who made it available.

Simple things should be simple. Complex things should be possible.

Sunday, 8 January 2012

PDF Report and the Law of Demeter

I'm finishing a small project which uses Christian Haider's pdf4smalltalk to build report output using a Seaside influenced coding style. A report with a header, text and footer would be coded as...


| report |
report := PRwReport new.
report portrait.
report page: [:page |
page header string: 'This is a header'.
page text string: self someText.
page footer string: 'This is a footer'].
report saveAndShowAs: 'TestText.pdf'.


The tool supports the usual report output options, like fonts, alignment, tables, images, bullets and so on.

I've built a couple of other Smalltalk report frameworks over the years. One used Crystal Reports for the layout with configurable data gathering, and another (much better tool) that used Totally Object's Visibility (I did a presentation on that one at Smalltalk Solutions 2004). Both of those used a data + layout spec model, which, with the benefit of hindsight, was not be best choice. It was a challenge to keep the code and layout in sync. Maintenance was painful. For PDF Report I opted for Seaside's 'paint the content on a canvas' pattern. It is working nicely (I'll be presenting the details at Smalltalk Industry Conference in Biloxi). 

Here's the part that got me thinking about how nice objects are and the Law of Demeter...  when building a report output, you have to deal with coordinating the size and position of layout components on the page. Do you give the responsibility to the page, or do you have the layout objects find their own place? I opted for a 'builder': it knows how much space is available on a page and which layout objects need to be processed.

The interesting part was in deciding how much the builder needed to know about each layout. The first few iterations were rudimentary: each layout had a calculated height (word wrapped text with a selected font) and the builder would output as much as would fit on one page, then trigger a page break and continue on to the next page.

But that did not work with tables, since each row could have some cells that spanned pages. The builder could not blindly trigger a page break on a tall cell, since the next cell would be on the previous page. The table, row and cell had to communicate layout information to the builder, with the cell width and height dependent on neighbouring cells. And, to make things especially interesting, tables can be nested and cells can span rows and columns, like this...

...and this...



Each time I added a new layout mix to the SUnit tests I had to rethink what each object knew. After several iterations a pattern emerged: the less the builder knew about the layout objects, the better. And as the builder got dumber, its code got simpler and new layout mixes just worked. A tricky part was in sequencing the layout calculation for nested objects: a cell's height is dependent on the row's height, but the row's height is the maximum of it's cell heights.

Once the calculation sequence was correct, each layout object was able to answer it's layout values: position, margin and padding. The builder could ask if a layout could fit in the remaining space on a page without knowing what the layout objects was (text, table, bullet, image or line) and could create a new physical page without knowing how a layout object would be split. Now each refactoring cycle starts with me asking myself: how can I reduce what the builder needs to know? The latest version is much cleaner than the first. It's nice to apply well known object design rules and see real results.

Still a lot of work to do, but I'm looking forward to showing it at the conference. And, if it's good enough, it will be added to the VW public store (long term plans are to port to other Smalltalk dialects).

Simple things should be simple. Complex things should be possible.

Tuesday, 15 November 2011

Smalltalk head space

At the last Toronto Smalltalk User Group meeting we had a basic introduction to Seaside. It had been requested by some of the members, especially by the two profs that sponsor our group at Ryerson University. They teach an OO course with a Smalltalk component and felt it would be of interest to their students. A few did show up and the feedback was very positive. A bit surprising, given how basic the demo was (here is a link to the PDF), but it got me thinking about how easy it is to get out of touch with how other people see things. There is something to be said about baby steps.

After the talk we spent a while talking about the sad sate of computer science education; how it's become a C syntax programming training school. Way back in my day (early 80's) there was no dominant computer language. Intro to comp. sci. was taught in FORTAN. We learned BASIC, PL/I, COBOL, SNOBOL, Lisp, 360 Assembler, Prolog and Smalltalk. My OS course was taught as a history lesson, explaining why and how each OS concept was introduced, and why some were abandoned. There was a big focus on 'why', not just 'how'. That does not seem to be the case today.

During the conversation, I talked a bit about how selling Smalltalk can be a challenge. Smalltalk is different. If all you know is C and Java, it's really different. Why learn something different if it will not help you get a job? I made the case that learning Smalltalk helps you learn how to think in objects. A perspective that may be helpful when dealing with the other object hybrid languages (it would also be good to learn Lisp, Prolog and assembler). You can't help thinking that a Java-centric education gives students only one tool: a hammer, and they then view every task as a nail.

But how do you communicate the value of a "Smalltalk head space"? A world where everything is a live object that you can see and change. A place where delegating behaviour to Integer and String makes perfect sense; where the language is minimalist and the libraries add the complexity; where working with a debugger feels natural. And how do you explain how sending messages is different than calling a function? The mechanics are simple enough, but the mental model is harder.

I see functions as a handing off of total control of the world to something else. Within a function you change whatever data you need to. The world is a large, porous bit universe that you manipulate. Whereas sending a message is asking another object to do something for you. You don't think about tickling the bits of another object. You do your job, send messages to other objects, and maybe answer something. You work in a  local scope, not the whole world.

Every professional Smalltalk developer I know can tell talk about their "aha" moment, the point where object-think made sense. In my case it was a course at The Object People where Paul White was explaining how to model a transfer between a chequing and savings account: asking either account to do the transfer smelled wrong, so instead you could model the transaction. Really? You can do that? A 'transction' can be an object? Cool. A more whimsical example is modelling the milking of a cow. Do you ask the cow to milk itself? Do you ask the milk to un-cow itself? No. You model a farmer to do the milking 'transaction'.

The problem is that those "aha" moments take time to learn. And who has time for that? As Dave Thomas said in his recent SPLASH video interview, the complexity that we've added to the development tools is not always a benefit to someone building a simple application. Do you really need to know about objects if a BASIC program will do? Is learning about classes and instances to big a hurdle? Would a prototype object model be easier to learn? I don't know, but we really have to find a better way to communicate the up side of using Smalltalk. Waiting for each student to experience their own "aha" moment is not good enough.

Simple things should be simple. Complex things should be possible.

Monday, 12 September 2011

XML RESTful Seaside interface to legacy system

I'm mandated with creating web interfaces to two legacy frameworks, one written in VA and the other in VW.

For the VW framework I started with a full application implementation; a Seaside interface that duplicated the entire application interface by parsing VW window specs and building Seaside components. But for both the VA and VW systems there is also a need for a 'portal' web site, one that exposes a limited view of the application but is intended for a larger user base.

Both systems were designed over a dozen years ago, using GemStone and a fat client. Grafting a multi-user Seaside interface to the fat client designed for a singe user was not going to work. That's why the VW 'full user' implementation uses one 200MB VW image per Seaside session, a deployment model that was not an option for the portal: too many users (a resource issue), too much exposed (a security concern), too integrated (a maintenance problem).

To work around these constraints I've implemented an XML based domain interface, where a Seaside image can gather the data it needs to render using a RESTful interface to GemStone. There is no domain model in the Seaside image, just components that know what part of the domain they represent. With GemStone this works particularly well since it eliminates the need to fault objects into the client. It's a common technique with GS, popularised by James Foster, to build strings on GS, fault them to the client, then parse and display the content.  Some displays, like lists of objects, can improve their performance from tens of seconds to sub-second.

Each Seaside component knows which dedicated server method to use. Since the portals have a limited scope there are not that many XML data methods. I would not use this approach if I expected the portal application scope to increase significantly.

All of this was made easier by thinking of the final rendering as a limited set of display patterns: lists, tables, trees, image and a single domain object. The XML has tags that identify the display pattern: <list> <table> <tree> <image> <domain>. These are used to build 'XmlObject' subclasses for each pattern. This makes accessing and debugging data easier than referencing the parsed XML code directly in the Seaside components.

Parsing XML is very different between VA and VW. Wrapping the results in my own XML node objects made it easy to port my code between dialects. I just needed to abstract out the XML parser references. Predefined tags, like 'oop, objectclass, text' are stored in instance variables. Any other tags are stored in a dictionary. The Seaside component would expect certain tags to be present and will trigger an error otherwise.

Some examples...

domain
- only attributes the Seaside component needs are included
- used in the list, table and tree patterns with minimal attributes; if selected the oop is used to get whatever else is needed
<domain>

<oop>461206257</oop>
<objectclass>AlaItemWorksheet</objectclass>
<text>ABTP Lab Results - D - DEN</text>
<icon>AlaDataEntry.ico</icon>
<label>ABTP Lab Results - D - DEN (TAB [Final Effluent])</label>
</domain>



list
- stored as named lists in the collection of attributes for a domain node
- each element of a list is another domain node, with enough information to be displayed (label, icon) and with the domain object oop in case it gets selected
<list type="actions">

<domain>
<oop>291900165</oop>
...
<domain>
<oop>291913433</oop>
...



table

- added tags for header and styles. The style contents is encoded as key, value pairs so that the server code can include style information that a generic table component will render
<table>


<header>
<data>
<value>ABTP Lab Results - D - DEN</value>
</data>







tree
- each tree node knows how to get its sub-tree
- sub-trees are retrieved and cached when a tree node is expanded
<list type="productSet">


<domain>
<oop>199226369</oop>
<objectClass>INproductSet</objectClass>
<text>By Function</text>
<hasSubtree>true</hasSubtree>
<hasProducts>false</hasProducts>
</domain>


A Seaside component is created for each tree node which reads the 'hasSubtree' attribute and, based on a configuration, gets the nested list of domain nodes when expanded.




image
- for the VA implementation, images are stored as byte arrays on GS
- for the VW, the images are stored as server paths
- in each case, the image node needs to answer a byte array that can then be rendered
<domain>

<oop>249729481</oop>
<objectClass>AlaSiteMapImage</objectClass>
...
<image>
<oop>249728229</oop>
<mimeType>image/gif</mimeType>
</image>



Building the XML on the server is done with a 'canvas' object which wraps tags around indented nested content. Formatting of the XML content is a debugging convenience.

To add a domain object...


aCanvas 
addDomain: anItem 
text: anItem product id
with: [
aCanvas add: 'description' put: anItem product description.

...


To add a list...

aCanvas 
addListNode: productSet
type: 'productSet'
with: [
collection do: [:each | self buildXmlProductSet: each on: aCanvas]].


For both apps a user identifier, in the form of a GS object oop, is included in each data request. Seaside stores the user object oop in the WASession subclass instance variable. Communication between the Seaside image and GS is behind a firewall, so I'm not that concerned about it being monitored.

RESTful communication works well with my Smalltalk message passing sensibilities. Objects passing messages feels so natural, and debugging is easy since I can record and replay any message. And I don't care what gets changed on the server, as long as the Seaside XML methods answer the same way.

Although the code is written in both a VW and VA client, it's dialect agnostic; they could be swapped. I stuck with the two Smalltalks because I was replacing a domain model Seaside implementation in each, and there was enough sunk cost to leave that code as is.

Ideally I would like to host the Seaside sessions from GS with a GLASS deployment.  It's a long term option with the VW application, but the VA application has to be hosted on a Windows server, which limits us to 32 bit GS and no GLASS.

So far testing is going well. The next step is to test this under load and see how many concurrent Seaside sessions one image can handle. We already have a multi-Seaside image dispatcher implementation working with Apache that supports session affinity, so I'm confident that scaling will not be a problem.

Simple things should be simple. Complex things should be possible.

Wednesday, 10 August 2011

Seaside with HTML5 for iPad & iPhone

We did a demo recently on a iPad showing how, with HTML and some javascript, we could make a Seaside client web site look native-ish. It's impressive what a minimalist layout, large fonts and big buttons can do.

This is new system development: the client had already seen a simple Seaside 'portal' web site which gave employees access to their shifts, with selected shift details and client information (each shift is a service to a specific client). All done as one integrated system for which they have a VW fat client.

For the iPad and iPhone we wanted to be aware of the screen real estate, include some device orientation sensitivity and to make sure that all selections were fat figure friendly (I'm a good tester for the 'fat finger' part).

For device orientation, you can use @media rule in CSS, or javascript, which is what I did. I set up a landscape and portrait DIVs and then used javascipt to hide and show the appropriate one. The layout was simple: in landscape I have a 75% / 25% horizontal split. In portrait right 25% area is rendered below in a wide layout. You can do some cool stuff with orientation. In most cases you don't what the display to flip around too much. But you can set up a image, like a logo, that responds to each small tablet movement on any axle. I didn't use that in the demo, but it looks cool.

This is the orientation script I used.

if (window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', function(eventData) {
deviceOrientationHandler();
}, false);
} else {
       makeLanscape();
}
function deviceOrientationHandler() {
if ( orientation == 0 ) {
makePortrait();
}
else if ( orientation == 90 ) {
makeLanscape();
}
else if ( orientation == -90 ) {
makeLanscape();
}
else if ( orientation == 180 ) {
makePortrait();
}
}
function makePortrait() {
document.getElementById("tabletLandscape").style.display = 'none';
document.getElementById("tabletPortrait").style.display = 'block';
}
function makeLanscape() {
document.getElementById("tabletLandscape").style.display = 'block';
document.getElementById("tabletPortrait").style.display = 'none';
}

And this does the funky real time image orientation, with an image with id "imgLogo".
if (window.DeviceOrientationEvent) {
  window.addEventListener('deviceorientation', function(eventData) {
        var LR = eventData.gamma;
        var FB = eventData.beta;
        var DIR = eventData.alpha;
        deviceOrientationHandler(LR, FB, DIR);
}, false);
} else {
alert("Not supported on your device or browser.  Sorry.");
}
function deviceOrientationHandler(LR, FB, DIR) {
   //for webkit browser
   document.getElementById("imgLogo").style.webkitTransform = "rotate("+ LR +"deg) rotate3d(1,0,0, "+ (FB*-1)+"deg)";
   //for HTML5 standard-compliance
   document.getElementById("imgLogo").style.transform = "rotate("+ LR +"deg) rotate3d(1,0,0, "+ (FB*-1)+"deg)";
}

To get the screen size, I dug up some code that Lukas had posted on how to get screen size from the browser. It's one of those code snippets that really helped me understand how Seaside gets the browser to trigger callback blocks on the server.

renderScreenSizeOn: html
self screenX isNil ifTrue: [
self screenX: 0.
self screenY: 0.
html
script: ('window.location.href="' , html context actionUrl asString ,
'&' , (html callbacks registerCallback: [:v | self screenX: v asNumber]) , '=" + screen.width + "'
, '&' , (html callbacks registerCallback: [:v | self screenY: v asNumber]) , '=" + screen.height') ].
html text: self screenX printString, ' x ', self screenY printString

...which generates the script...

window.location.href="/Test?_s=sOMeODiqoU20GBt4&_k=pbWTZltjW35uekru&1=" + screen.width + "&2=" + screen.height

So, I triggered href="/Test?_s=sOMeODiqoU20GBt4&_k=pbWTZltjW35uekru&1=4288&2=1143 which Seaside reads and sends '4288' as the #value: to block [:v | self screenX: v asNumber] and '1143' to  [:v | self screenY: v asNumber].

If I edit the URL and change the values in the same session...
 ?_s=sOMeODiqoU20GBt4&_k=pbWTZltjW35uekru&1=1234&2=4567
..my screen will render the view values: 1234 x 4567. Neat.

Anyway, it was simpler to just use the request 'user agent' information to redirect to different applications, in the same image, for each device. This also made it easy to test the tablet and phone URLs on a full browser by using the appropriate URL.

renderContentOn: html | string |  string := self requestContext request userAgent.
(string includesSubString: 'iPad') ifTrue: [^self requestContext redirectTo: self tabletURL].
(string includesSubString: 'iPhone') ifTrue: [^self requestContext redirectTo: self phoneURL].
(string includesSubString: 'webOS') ifTrue: [^self requestContext redirectTo: self tabletURL].
^self requestContext redirectTo: self portalURL

I subclasses most of my components with *Tablet and *Phone suffixed classes, which made for easy code reuse and kept all of the device specific code in Seaside.

The portal required some diagram annotation, so I used javascript to detect figure movement and added more javascript for mouse movement. This way images could be annotated from any device. I started with a canvas ...


html div id: 'container'; with: [
html canvas id: 'sketchpad'; width: 350; height: 350; style: 'border: 1px solid black; background: white';
with: [html strong: 'Your browser does not support canvas.']].

...and then used this script for the iPad & iPhone figure drawing...

// get the canvas element and its context
var sketchpadCanvas = document.getElementById('sketchpad');
var context = sketchpadCanvas.getContext('2d');
context.fillStyle = 'red'; // red
context.strokeStyle = 'red'; // red
context.lineWidth = 4;
//Load the image object in JS, then apply to canvas onload
var myImage = new Image();
myImage.onload = function() {
context.drawImage(myImage, 0, 0, 350, 350);
};
// create a drawer which tracks touch movements
var drawer = {
  isDrawing: false,
  touchstart: function(coors){
     context.beginPath();
     context.moveTo(coors.x, coors.y);
     this.isDrawing = true;
  },
  touchmove: function(coors){
     if (this.isDrawing) {
        context.lineTo(coors.x, coors.y);
        context.stroke();
     }
  },
  touchend: function(coors){
     if (this.isDrawing) {
        this.touchmove(coors);
        this.isDrawing = false;
     }
  }
};
// create a function to pass touch events and coordinates to drawer
function draw(event){
  // get the touch coordinates
  var coors = {
     x: event.targetTouches[0].pageX,
     y: event.targetTouches[0].pageY
  };
  // pass the coordinates to the appropriate handler
  drawer[event.type](coors);
}
// attach the touchstart, touchmove, touchend event listeners.
sketchpadCanvas.addEventListener('touchstart', draw, false);
sketchpadCanvas.addEventListener('touchmove', draw, false);
sketchpadCanvas.addEventListener('touchend', draw, false);
// prevent elastic scrolling
document.body.addEventListener('touchmove',function(event){
 event.preventDefault();
},false); // end body:touchmove

To work on a non-touch browser, I loaded scripts to do the annotation with a mouse based on this tutorial.

Setting a background image, which also cleared the annotation, was done with a script
myImage.src = "/files/TSwaFileLibrary/body.png";
Saving the image was done with #onClick: script from a button.... the 'toDataURL()' is the interesting bit.
html jQuery ajax
serializeForm;
callback: [:value | self saveImageFile: value]
value: (Javascript.JSStream on: 'sketchpadCanvas.toDataURL()')

...and... (removing the first 22 characters is a hack to strip out the ''data:image/png;base64,'' MIME declaration). The code is in VW.
saveImageFile: anImageString
| writestream string | writestream := (self newImageFilename asFilename withEncoding:  #binary) writeStream.
string := anImageString copyFrom: 23 to: anImageString size.
[writestream nextPutAll: (Seaside.GRPlatform current base64Decode: string) asByteArray] ensure: [writestream close].
The demo went well. I'm always surprised how making something pretty makes people think it's better. In this case I just added a new facade on a web site, and it was considered a big thing, yet it was only a few days work. It's like my buddy in marketing says: ya gotta sell the sizzle, not the steak. There's a lesson in there somewhere for Smalltalk.

(here is a good summary of iPod HTML5 javascript development)

Simple things should be simple. Complex things should be possible.


Sunday, 3 July 2011

Simple jQuery contact application

I continue to be impressed with how easy it is to build nice, useful applications with Seaside using the jQuery support.  We have an old Unix character based contact application that those who don't want to change (and you know how you are) continue to use. I gave up on character based interfaces the late 80's, so I created a simple Seaside app to access the contact data.
The contact data is dumped daily into a special character delimited file, with 40 fields per row. Trivial to parse in Smalltalk...
array := aString subStrings: 1 asCharacter.
...where aString is one row of the data file.

Accessing the fields is just an array access, like...

postalCode
^self dataArray at: 16

The collection of contacts (about 8500) is stored in a contact library object, in three collections, each sorted by a key stored on the object as a lower case string (by first name, by last name and by company). At first I tried using some fancy nested tree structure, then I tried large complex key dictionaries, but doing a match on 8500 instances takes less than 10 ms. Trying to optimize that just added complexity. Storing the search keys as lower case strings rather than translating them on each iteration was helpful. Search times dropped from 60 ms. The search method also has a limit to keep the auto-complete list is to a reasonable size. A '*' is appended to the string to support the general 'match' case.

string := aString asLowercase.
string last = $* ifFalse: [string := string copyWith: $*].
list := aCollection select: [:each | string match: each nameKey].
list size > limit ifTrue: [^list copyFrom: 1 to: limit].
^list

We match on #nameKey, but the displayed list uses #displayFullName, the mixed case version of the string.

OK, so the domain in as simple as it gets. The fun part was the Seaside code. JQAutocomplete>>search:labels:callback:  made it easy (props to its author). The method wraps a lot of complexity into a tight, useful package. In my case, the code looks like this...

html textInput
id: anId;
style: 'width: 300px; font-size: 14pt';
script: (html jQuery this autocomplete
search: [:string | self buildSearchResponse: aSearchBlock with: string]
labels: aLabelBlock
callback: [:value :script | self redirectToContact: value script: script])
#buildSearchResponseh:with: is a hack to show error messages in the auto-complete list. An exception answers an error object which is a subclass of the contact class, which is then displayed in the drop-down list.

^[aBlock value: aString]
on: Error
do: [:ex | Array with: (TSroloError newForErrorMessage: ex displayString)]

#aLabelBlock uses one of the three display methods for the drop down list (#displayFullName #displaySurname #displayCompany).

#redirectToContact:script: shows a nice URL with a ?contact=nnnn suffix.

| urlString |
urlString := aScript requestContext request url startingURL ,
'?contact=',
aContact displayContactIndex.
aScript goto: urlString.
self session unregister
In #renderContactOn: I check for the 'contact' parameter and render the contact details if found.

Here is what it looks like...
...typing 'bob n' into the full name fields shows...
...selecting 'Bob Nemec' then redirects to '?contact=3784', which shows the contact details...
...the '+' toggles a display of all the contact fields, using the jQuery toggle...
| moreId lessId |
moreId := html nextId.
lessId := html nextId.
html anchor
id: moreId;
onClick: (
html jQuery ajax script: [:s |
s << (html jQuery id: aComponentId) toggle: 0.3 seconds.
s << (html jQuery id: moreId) hide.
s << (html jQuery id: lessId) show]);
title: aMoreTitle;
with: [html image url: TSwaFileLibrary / #morePng].
html anchor
id: lessId;
style: 'display: none;';
onClick: (
html jQuery ajax script: [:s |
s << (html jQuery id: aComponentId) toggle: 0.5 seconds.
s << (html jQuery id: lessId) hide.
s << (html jQuery id: moreId) show]);
title: aLessTitle;
with: [html image url: TSwaFileLibrary / #lessPng]

Finally, reloading the list of contacts is done by adding a ?load parameter, which loads the contacts from a predefined file location. This way the contact load action does not need to be done by the application, but can be done by an OS scheduled task.

And again, a big thanks for the Seaside jQuery code which makes writing simple apps like this easy.

A bad day in [] is better than a good day in {}