OnlineTester: An Arcane Adventure. Part 3: Application, Widgets and View Methods

Tagged:  •    •  

Let's hitch a ride with an incoming request to our designated test URL http://localhost:4080/onlinetest and take a look at the scenery. Be warned, though, that I'm easily scared and tend to close my eyes when passing through a tunnel.

From application ...

The initial request to our application is handled by Swazoo (supplied by GNU smalltalk), analyzed and dispatched (eyes closed, I told ya) to an instance of the Iliad.Application subclass, which claims to be responsible by claiming the path for itself in a class method:

Iliad.Application subclass: OTApplication [
   OTApplication class [ 
     path [ ^ '/onlinetest' ]
   ]
 ]

So we're in "our" code now? What happens next? By default, Iliad tries to get the name of a view out of the path following our application path. Since the initial request does not have such a thing, the default view method index will be used. The result of evaulating the view will be embedded in a page, which can be tailored to your requirements by overriding the default implementation of

  updatePage [ |e|
     e := self page headElement.
     e javascript source: '/js/jquery-1.3.2.min.js'.
     e javascript source: '/js/jquery-ui-1.7.2.custom.min.js'.
     e javascript source: '/js/iliad.js'.
     e stylesheet href: '/css/smoothness/jquery-ui-1.7.2.custom.css'.
     e stylesheet href: '/css/ot.css'
   ]

View methods return a block taking a single argument, which supports the necessary methods for building your web page. Our index method hints at two ways of working with such an element.

  index [ 
     <category: 'views'>
     ^ [ :e |
       self buildTitleOn: e.
       e build: self registerWidget ]
   ]

How do you distinguish "public" view messages from "internal" messages not fit for public consumption? Iliad does the sensible thing and leaves it up to you while providing a good default. Go read the class comment for Iliad.Application for further details, but rest assured that only methods in the category views are used as such.
You can either add XHTML elements to such an element:

buildTitleOn: anElement [ 
   anElement div 
     id: 'titel'; 
     h1: self test name; 
     h2: self test datum.
 ]

or tell it to build another widget in itself. I chose "in" instead of "on" itself, because the XHTML for the widget will be wrapped inside the tags of the containing element. Our registerWidget is built like this:

registerWidget [ 
   ^ registerWidget ifNil: [
     registerWidget := OTRegisterWidget callback: [ :username |
       ( self getTestWidgetFor: username ) ifNotNil: [ :w |
         self session preferencesAt: #widget put: w.
         self redirectToLocal: 'fragebogen'.
       ]
     ]
   ]
 ]

We're providing a callback with the actions specific for our application, so OTRegisterWidget is "generic", i.e. reusable.
Let's take a look at this simple widget next.

... to widget and ...

All of our application widgets are subclasses of Iliad.Widget. The definition starts with declaring a local variable, to be assigned by the constructor message.

Iliad.Widget subclass: OTRegisterWidget [ 
   | callback |
 
   OTRegisterWidget class [ 
     callback: aBlock [ 
       ^ self new callback: aBlock
     ]
   ]
 
   callback: aBlock [ 
     callback := aBlock
   ]

Next you can see how the contents of this widget are built:

  contents [ 
     ^ [ :e | | form uid |
       uid := self session nextId.
       form := e form class: 'entername'.
       form html: 'Name: '.
       form textInput id: uid; action: callback.
       form submitInput value: 'Test beginnen'.
       form script: ( self focusOn: uid ).
     ]
   ]

Again, a block is returned from the contents method, which takes the enveloping element as its single argument. We're adding a form with some text, a single line input field, a submit button and finally a bit of javascript to set the input focus into the text field so that the student taking the test can start typing her name immediately.

  focusOn: anId [ 
     ^ String streamContents: [ :stream |
       stream nextPutAll: '$(document).ready( function() { $("#';
         nextPutAll: anId displayString;
         nextPutAll: '").focus(); } );'.
     ]
   ]

The code above is my first bit of jQuery-based javascript and looks totally over-engineered for this application.
So why did we write it that way? First, this widget does not know anything about the calling application, it's reusable. This means that it could end up being used more than once on a single page. Hence we have to be a bit careful with things like CSS class names or HTML id attributes. The local variable uid takes care of that, as its assigned value is guaranteed to be unique to the page just being built. Second reason, well, I wanted to learn how to do it. Let's move on.
There's nothing left to move on to but the closing bracket of the class definition!

]

The key to why nothing more is required, is the unassuming, but essential line:

      form textInput id: uid; action: callback.

There we assigned the callback to be the action of the textInput field. All such callbacks are evaluated (with the fields' contents) when the form is submitted. The user clicks on the button, the callback is run and we go ...

... back to application

We're still talking about the instantiation of the OTRegisterWidget:

    registerWidget := OTRegisterWidget callback: [ :username |
       ( self getTestWidgetFor: username ) ifNotNil: [ :w |
         self session preferenceAt: #widget put: w.
         self redirectToLocal: 'fragebogen'.
       ]
     ]

Now that we know where the argument username comes from, the following code will make more sense. We're building a test widget for the student and keep it in the session for easy reference. Then we redirect to the URL http://localhost:4080/onlinetest/fragebogen, which is where the real action happens. What is left for the application is bookkeeping, not to be underestimated.

getTestWidgetFor: aNameString [ 
   <category: 'private - actions'>
   ^ self registry at: aNameString ifAbsentPut: [ OTTestWidget for: aNameString on: self test ]
 ]

The student's personal test widget is not only returned for keeping it in the student's session, but it is also kept for further reference in a registry maintained by the single application instancethat Iliad will automatically instantiate for you. The code for this is straightforward:

Iliad.Application subclass: OTApplication [
   | test testWidget registerWidget |
 
   OTApplication class [ 
     registry [ 
       ^ registry ifNil: [ self flushRegistry ]
     ]
     flushRegistry [ 
       ^ registry := Dictionary new
     ]
   ]
 
   registry [ 
     ^ self class registry
   ]
 
   test [ 
     ^ test ifNil: [ test := OTTest current ]
   ]
   test: aTest [ 
     test := aTest
   ]
 
   user [ 
     ^ ( self session preferences at: #widget ifAbsent: [ ^ nil ]) username
   ]
 ]

The final part of our application class is the view for the fragebogen path:

  fragebogen [ 
     <category: 'views'>
     ^ [ :e | 
       self user
         ifNotNil: [ e build: ( self getTestWidgetFor: self user ) ]
         ifNil: [ self redirectToLocal: '' ]
     ]
   ]

If we have a student, show her test widget. If we don't, let the student register herself. Note that our approach has implications: On the one hand, it is easy to 'get back' your test widget, even after a browser crash, just by entering your name again. On the other hand, it's equally easy to get another student's test widget ... but hey, we still need a reason to keep the teachers on the payroll :->

'Nuff said for tonight, I hope to post some more tomorrow.

You can use Iliad.Application>>redirect* methods directly, (#redirectTo, #redirectToLocal and #redirectToIndex). Also, there are preferenceAt:* methods in Iliad.Session.

Nico

Thanks for pointing these out, I'll change the code accordingly.
For the record, I had referenced the session explicitly when redirecting and assumed that session preferences answers a Dictionary-like object when I should have been allowing session to encapsulate this implementation detail.

User login