A Walk on the Seaside

This tutorial is an exploratory introduction to the Seaside web framework. Most of the material should be applicable to Seaside 2.7 to Seaside 3.0, although you may notice some minor differences.

Getting Seaside

If you don’t yet have a copy of Seaside, first download the Seaside One-Click Image from the Download page.

If you install Seaside into an existing Smalltalk image yourself (e.g. from SqueakMap), it will ask you to pick a username and password - these are used by Seaside’s configuration app, which we’ll get to later on.

Once everything is installed, you need to start the Seaside service running. If you’re using Seaside 3.0, you can use the Seaside Control Panel (in the One-Click Image, this is found on the World menu under Tools>More>Seaside Control Panel). Select a server and start it; if there are no servers listed, right-click on the top panel and select “Add adaptor...”, then choose “Comanche Adaptor”, and port number 8080.

control-panel

In versions of Seaside prior to 3.0, you will need to control the server classes directly. The WAKom class offers a simple interface to the Comanche web server. To start up an instance of Comanche configured with Seaside, evaluate the following piece of code in a Workspace:

WAKom startOn: 8080

Seaside should now be listening for requests on port 8080. Whenever you save and restart this image from now on, the Seaside service will come up automatically.

Basic Concepts: Sessions and Components

To test that Seaside is running, point your browser to http://localhost:8080/seaside/examples/counter (or http://localhost:8080/seaside/counter for versions older than 3.0). This should bring up the most minimal of Seaside applications: a simple counter with increment and decrement links. We’re going to be poking around this little application for a while, but for now just play with it and make sure it works: click the "++" link to make the number increase, and the "—" link to make the number decrease. Counter

As you play around with the counter, look at the URL in the location bar of your browser. The URL has two query parameters, each of which can tell us something about how Seaside works. First, there is a long, random looking series of letters and numbers, that looks something like _s=ZXsNyDAuodddtmEG, and never changes as you use the application. This is the unique key which identifies the current user session. The session is a central concept to a Seaside application. Unlike in most web frameworks, which provide a session object only as a place to keep state, and focus mostly on individual request/response cycles, Seaside treats a session very much like a thread or a process: it starts a session running at a well defined entry point, and the application proceeds linearly from there, pausing to display web pages to the user and wait for input. We’ll look at this more closely when we discuss control flow, later in the tutorial.

You’ll notice a link named "New Session" on a grey bar that extends along the bottom of the page. This is the Seaside toolbar, which appears during development; we’ll explore some of its options as we go along. The "New Session" link makes it easy to ditch the current process and start a new one from scratch. If you press it now, the only things you’ll notice are that the counter gets reset to 0, and the session id changes.

The second parameter (identified by _k) is a shorter, random string of letters and numbers. This parameter doesn’t refer to a particular action, or encode the session state; all state is kept on the server side, and a Seaside session progresses linearly, not by jumping around to this or that action. Instead, it simply keeps track of requests in the current session, and allows Seaside to track the user’s progress through the application. Similarly, each link or form field on the page is given a unique, sequential id: meaning is only attached to these numbers when they get back to the server. This has the disadvantage of rendering bookmarks unintelligible or even unusable, but it provides a huge amount of flexibility.

Instead of named pages or actions, Seaside applications usually consist of interacting "components", objects which model the state and logic of a portion of the UI. When the session starts, a single component instance is created, and that component enters a response loop: it displays itself to the user, and then waits for input. The input (a clicked link or a submitted form) will trigger a corresponding method on the component, and when that method is done, the component will display itself again. These triggered methods may simply update the state of the application or of the current component, or they may create and transfer control to another component, which will enter a response loop of its own.

The component class that models the "counter" application is WACounter, and we’ll take a closer look at it now.

State, Action, Display: Component Essentials

Each component has three responsibilities: maintaining UI state, reacting to user input, and displaying itself as HTML. We’ll take a quick tour through how WACounter handles each of them.

State

Even if much of the state of an application is stored in business objects or a database, the user interface often has state of its own. This might include, for example, the current value of a form field, or which database record was being displayed, or which nodes of a tree view are currently expanded. All of these are stored in the instance variables of the components that make up the UI.

In the case of WACounter, the only state to worry about is the count itself. When the instance of WACounter is first created at the beginning of the session, it initializes its ’count’ instance variable to 0. By clicking on the "++" and "—" links, you’re simply changing the value of this instance variable. A good way to investigate this is by clicking on "Toggle Halos" link in the toolbar, which brings up a set of extra debug icons. Click on the magnifying glass icon that appears at the top of the page. This will bring up a web-based inspector on the ==WACounter== component. You’ll see a ==WACounter== with a number of instance variables inherited from its superclass, ==WAComponent==, but you’ll also see ’count’ at the end. The current value of ’count’ will always be the same as the count that was displayed on the page from which you launched the inspector.

If you want to explore, the link on an instance variable’s name will take you to a new inspector for that object; you can use the breadcrumbs to walk back up the tree. When you’re finished, just close the inspector window. To make the extra icons go away, you can click "Toggle Halos" again.

Action

There are two possible user actions in the counter application, and WACounter has a method corresponding to each of them. Clicking on the "++" link will result in a call to ==#increase==, which is a very simple method - here it is in full:

increase
count := count + 1

This does exactly what you would expect: it increases the value of ’count’ by one. When it returns, the response loop continues, and the component displays itself again with the new value.

For fun, let’s try modifying this. Beside the magnifying glass icon on the halo is a pencil icon - pressing this will take you to a very functional web-based System Browser (courtesy of Lukas Renggli), open to the class of the current component. Find the #increase method and change it so that count increases by two instead of by one. Remember to hit Accept. Then close the browser and try the "++" link again.

Display

When Seaside needs to display a component, it sends that component the message #renderContentOn:, passing an instance of WARenderCanvas as the argument. This will be a familiar pattern to anyone who has implemented an applet in Java, or a new subclass of Morph in Squeak: the object is given a canvas, which it is then expected to use to display itself. In this case, rather than shapes and lines, the "canvas" knows how to render elements of HTML. The renderer works like a stream: each message to it will append some text or elements to the document being rendered. Here’s WACounters #renderContentOn: method:

renderContentOn: html
html heading: count.
html anchor
callback: [ self increase ];
with: '++'.
html space.
html anchor
callback: [ self decrease ];
with: '--'

Three different messages are sent to the renderer, each of them producing a different kind of HTML element. The first of these, #heading:, produces a simple section heading: if the current value of ’count’ were 42, it would append 42 to the document. #space is also very simple, simply appending a non-breaking space character. #anchor is more interesting. It is called, obviously, to produce the "++" and "—" links that appear under the count. And the ==with:== argument is clearly specifying a string to appear in the link. But what do these links point to? Where do they go?

The short answer is, don’t worry about it. In Seaside, links don’t have destinations, they have callbacks: each time you generate a link or a button, it is associated with a block. When that particular link or button is clicked, the block is evaluated. In this case, clicking on the "++" link will result, as you would expect, in a call to ==self increase==.

Let’s modify this method slightly: instead of links for increment and decrement, we’ll use submit buttons. Find WACounter>>renderContentOn: in a Squeak browser, and change the code to this:

renderContentOn: html
html form: [
html heading: count.
html submitButton
callback: [ self increase ];
text: '++'.
html space.
html submitButton
callback: [ self decrease ];
text: '--' ]

The main thing we did was to change the calls to #anchor to submitButton. The two methods are used almost identically: it’s very easy to switch back and forth between links and buttons as your interface requires. However, submit buttons will only work if they are inside a form. The #form: method is very simple, taking a singe block. This time, it’s not a callback; instead, it’s used structurally, to enclose the contents of the form. Using #form: this way simply ensures that everything ends up inside a pair of form tags. The same convention is used by the table methods, #table:, #tableRow:, and #tableData:.

The Response Loop

Now that you’ve been introduced to each of a component’s responsibilities, it may be useful to return to how they’re all related. Each component operates a response loop, displaying itself, waiting for input, processing the input, and then displaying itself again. In terms of the methods we’ve seen so far, it looks like this:

  • An instance of WACounter is created; ’count’ is initialized to 0.
  • Seaside calls #renderContentOn: on the counter. This produces an HTML document displaying the current value of ’count’, as well as some links with associated callbacks. This HTML is sent to the user.
  • The user clicks on one of the links, invoking the associated callback, which in turn calls either #increase or #decrease. This sets ’count’ to a new value. The action method then returns.
  • The counter is rendered again, with its new state.

Looking Deeper: Interactions Between Components

To transfer control to another component, WAComponent provides the special method #call:. This method takes a component as a parameter, and will immediately begin that component’s response loop, displaying it to the user. To test this, we can use the WAFormDialog component class - we’ll modify #decrease to print a message if the user tries to go below zero:

decrease
count = 0
ifFalse: [ count := count - 1 ]
ifTrue: [
self call: (WAFormDialog new
addMessage: 'Let''s stay away from negatives.';
yourself) ]

If ’count’ is zero, this creates a new instance of WAFormDialog, gives it an informative message to display, and calls it. Now start a new session and try to hit the "—" link. The counter should disappear, and you should see the dialog instead, the message shown as a large heading.

Displaying a simple dialog like this is common enough that WAComponent provides an #inform: method for it, mimicking the default #inform: provided by Object. Try changing #decrease to use it, like so:

decrease
count = 0
ifFalse: [ count := count - 1 ]
ifTrue: [ self inform: 'Let''s stay away from negatives.' ]

You may notice that along with the message, the dialog has a button labelled "OK". What happens when you press that? Well, the dialog goes away and the counter is displayed once more. Behind the scenes, the instance of WAFormDialog invokes a companion method to #call: named #answer, which causes control to return back to the calling component. In effect, calling another component is a simple subroutine call: if you like, you can think of #call: as pushing a new component onto the stack, and #answer as popping back to the old one.

In fact, no such stack is maintained. Instead, what #answer does is a little more complicated: it causes the original message send of #call: to return, and the program continues from that point. Since calling the dialog was the last statement of #decrease, all that happens is that decrease returns and the counter’s response loop continues.

This is completely non-intuitive, and needs a lot of further explanation. Let me break down the sequence of exactly what happens:

  • WACounter>>decrease makes a call to WAComponent>>inform:
  • WAComponent>>inform: creates a new instance of WADialog, and passes it into WAComponent>>call:. Let’s name this point in the sequence "the call point".
  • The WAFormDialog begins its response loop, and displays itself to the user’s browser.
  • The user clicks the "OK" button. This causes WAFormDialog to invoke WAComponent>>answer.
  • Now for the strange part. WAComponent>>answer never returns. That’s because it makes a jump: control shifts back to the "call point", just after the send of #call:.
  • WAComponent>>call: returns to WAComponent>>inform:.
  • WAComponent>>inform: returns to WACounter>>decrease.
  • WACounter>>decrease returns to the counter’s response loop, and it is displayed.

The really cool thing about #call: is this: if a called component provides an argument to #answer:, that argument will be returned from #call:. In other words, calling a component can yield a result. This is much more powerful than simply pushing and popping components from a stack. For example, it makes it easy to implement #confirm:, which displays a question and returns "true" or "false" depending on what the user clicks, to go with #inform:. Try changing #decrease as follows:

decrease
count = 0
ifFalse: [ count := count - 1 ]
ifTrue: [
(self confirm: 'Do you want to go negative?')
ifTrue: [ self inform: 'Ok, let''s go negative!'.
count := -100 ] ]

If you play with the counter now, you’ll realize that within this one method there can be up to three page views: the confirmation dialog, the message dialog for "Ok, let’s go negative", and finally back to the counter itself (for bonus points, try using the back button during this sequence and see what happens). This is a typical structure for Seaside applications: rather than having a series of closely coupled pages, each of which knows which pages come before and after it, each page will collect and return a single piece of information from the user, with the logic stringing them together all maintained in one place. The result can be stunningly resuable pieces of code.

Call and answer isn’t the only way Seaside components get reused. You may not realize it, but there have been at least two Seaside components on your screen at all times. One of them is the WACounter - what’s the other? The answer is, what you’ve actually been seeing this whole time is an instance of WACounter embedded inside another component, an instance of WAToolFrame, which renders the toolbar. This sort of embedding is very common in Seaside, and pages are often made up of many individual, nested components. The details of embedding components are beyond the scope of this tutorial, but for a simple example you might want to look at WAMultiCounter (http://localhost:8080/seaside/multi), which contains several independent WACounters. If you do, make sure to hit the "Toggle Halos" link and poke around.

Moving On: Starting Your Own Applications

Hopefully, by now you have some sense of what a Seaside application looks like. To start playing with your own, you may want to look at the configuration app at http://localhost:8080/seaside/config. This makes it easy to add a new, named application, and associate it with a component class of your choice. You’ll need the username and password you picked at installation time to get in. If you write a new component and want it to show up as an option in the config app, make sure you implement #canBeRoot on the class side to return true.

For more information read the material referenced in Documentation and especially http://book.seaside.st/.

Feel free to email comments to the Seaside mailing list: you can sign up at http://lists.squeakfoundation.org/cgi-bin/mailman/listinfo/seaside. If you are looking for free hosting of Seaside applications check out http://www.seasidehosting.st.