Wanted -- A Seaside Tutorial: Part 4

In the last post we created our WantedItem model object and built a preliminary main page that lists the WantedItems stored in our WantedDatabase. In this entry, we'll refactor the WantedItem class a little bit, create an html table to display the wanted items, and make it look better. As usual, for those playing at home, open Squeak with the want-tutorial.image

Step 1: Refactor the WantedItem Class

At this point, our WantedItem is just a data holder. As Ramon Leon points out, having a constructor encapsulates how one should instantiate an object. Something else to notice is that the WantedItem class has an enteredDate, which is the date the WantedItem was created. We don't need to have the user assign this value--we know when a WantedItem object is created, so we can assign the value ourselves.

Select the WantedItem class in the Refactoring Browser, and click on the class button. Then select the "-- all --" message category so that the method creation template displays in the code pane. Enter the following code:


title: aTitle notes: someNotes
^ self new title: aTitle;
notes: someNotes;
enteredDate: DateAndTime now;
yourself


and save. We create a new WantedItem by passing it the new message, then set the fields on the new instance using the arguments the class message receives, then set the current date and time by passing the now message to the DateAndTime class. Notice that our new method went into a category named "as yet unclassified". Right-click or option-click the title:notes: selector in the message pane, select "more...", then "change category...". A large list of possible categories pops up. Choose "instance creation".



Now that we have a constructor that will create a new WantedItem with all the required fields, it's easier to create WantedItems and users of the class will have more confidence that they are creating an instance correctly.

One of the goals of this project is to figure out what items we really want based on how long we hold a desire for them. A WantedItem object instance already knows the date on which it was entered. If we provide the WantedItem class a with a time threshold, the instances can calculate whether or not they have passed the time test of being a true wanted item.

First we need to identify what the time threshold is. To do this, create a new instance side method on WantedItem called "daysToWait". Make sure the instance button is selected, then click on the "-- all --" message category and replace the code pane with the following:


daysToWait
^ 14


The daysToWait method returns the time threshold in days, which we have set to two weeks. The daysToWait method was put into the "as yet unclassified" category. Change the category to "defaults".

We then create a method called isPurchasable:


isPurchasable
^ (DateAndTime now - enteredDate) days >= self daysToWait


The isPurchasable method takes the current DateAndTime and subtracts the enteredDate DateAndTime from it. If you look at the "-" method in the DateAndTime class you will see that it returns a Duration object. Duration conveniently has a "days" method, which we use to compare against our daysToWait (14). If the item has been around the number of days to wait or longer, we can withdraw some money from the bank and get in trouble with the significant other. You'll be fine--just point them to the isPurchasable method.

Our WantedItem has grown a bit. Instead of just holding data, it now provides an obvious way for users to create it and knows whether it is a purchasable item or not.


Step 2: Create the HTML Table

On the main page of the Wanted app we want to display a list of wanted items: their titles, enteredDates and whether or not we can purchase them yet. Since we'll be displaying tabular data, it makes sense to use an html table. Seaside comes with a component that makes handling tables pretty easy: WATableReport. David Shaffer created a good tutorial on WATableReport based on some emails sent to the Seaside Mailing List from Radoslav Hodnicak and Dan Winkler. Basically you supply a WATableReport object with a collection of columns and a collection row data objects, and it will build the table for you.

Click on the WantedList class and make sure the instance button is selected. Add an instance variable named "wantedReport" and save:


Then click on the "-- all --" message category and replace the code pane with the initialize method:


initialize
| rows columns |
super initialize.
columns := OrderedCollection new
add: (WAReportColumn selector: #title
title: 'Title');
add: (WAReportColumn selector: #enteredDate
title: 'Entered Date');
add: (WAReportColumn selector: #isPurchasable
title: 'Can Buy');
yourself.
rows := WantedDatabase wantedItems.
wantedReport := WATableReport new rows: rows;
columns: columns;
yourself


and save. The first line is obviously the name of the initialize method. The second line contains two temp variables that exist for the life of the method--we introduce them to make it easier to work with the objects we will be using. Our WATableReport uses two collections to display data: a collection of WAReportColumns and a collection of row objects. The third line makes sure ancestors get initialized correctly. To create the columns, on the fourth line we create a new OrderedCollection by sending the OrderedCollection class the "new" message. Lines 5, 6, and 7 add WAReportColumns to that collection. Each WAReportColumn is constructed by sending the "selector:title:" class-side message. For selector, we pass a symbol (see this post for links to articles on Smalltalk syntax) representing the name of the message selector to call on each row data object. For title, we pass in the column heading that we want to display at the top of the table. Why is that "yourself" message there on line 8? We couldn't put a period directly after the last WAReportColumn because it would be returned by the parentheses and assigned to the columns temp variable. So we add a cascade operator after the last WAReportColumn, which will return the OrderedCollection, then send it the "yourself" message which just returns itself (you can't have the cascade operator as the last token). Line 9 assigns the WantedItems collection from our WantedDatabase to our rows temp variable. Lines 10 and 11 construct the WATableReport object using the columns and rows temp variables and assign it to the wantedReport instance variable. Line 11 returns yourself so that the columns temp variable is not assigned to the wantedReport intance variable.

Notice that when you saved the initialize method, it was assigned to the intialization category. When the WantedList component is created, the intialize method will automatically be called, building a WATableReport and assigning it to our wantedReport instance variable.

Since WATableReport is doing the heavy lifting to create the table html, our WantedList>>renderContentOn: method gets much smaller. Replace the renderContentOn: code with this:


renderContentOn: html
html render: wantedReport


and save. The renderContentOn: method just asks the wantedReport to render itself (build the html table).

Since WATableReport is a descendent of WAComponent, we now have a subcomponent in our WantedList. Seaside expects components to tell it when they have child components through the children method. Add the following instance-side method to WantedList:


children
^ Array with: wantedReport


The children method gets assigned to the "children" message category.


Step 3: Test the Table
We are now finally at a point where we can switch back to the browser and see what we've got. Point your browser to http://localhost:8080/seaside/wanted and you should see something like this:

So it's not the prettiest looking table in the world--we'll get to that in another post. Notice that the EnteredDate contains the date that we entered the item on, and that the "Can Buy" row value is "false". Let's test the isPurchasable method to see if it is working.

Flip back to the Refactoring Browser and open WantedItem>>daysToWait. Change the return value to 1. Since the comparison in isPurchasable is a greater than or equal test, this should pass all WantedItems. Save, then flip back to the browser and refresh. You should see "true" now for the "Can Buy" row value:

Make sure to change the daysToWait method back to returning 14.

What happens when we try to display multiple items in the table? What happens if we click on the column headers (they are links)?

First, to test out how the table will look with multiple rows of data, let's create some more wanted items and add them to the WantedDatabase collection. We can use the new WantedItem constructor. Open a workspace and enter the following:


WantedDatabase wantedItems
add: (WantedItem title: 'Big Screen TV'
notes: 'Need this for the Wii');
add: (WantedItem title: 'Extra Wii Controller'
notes: 'For playing with a friend').


Then highlight and "do it". Flip back to the browser and refresh. You should now see three WantedItem rows. Note that if you click on the headings, the table will sort by that column value. WATableReport gives us that for free. Wait a minute--did you try clicking on the "Can Buy" header column? You should have seen this:



In order to sort the rows when we click the "Can Buy" header column, it looks like the <= comparison message is being used. When the isPurchasable method is called for each row data item, a Boolean value (False in this case) is returned. The Boolean object does not have the <= comparison message. Flip back to the WantedList>>initialize method. You can see that the WAReportColumn that is created for the "Can Buy" column is using the selector:title: constructor. This is what is causing our problem: the isPurchasable selector is returning a Boolean value. Fortunately, the WAReportColumn has another constructor: renderBlock:title:. For each data object in the rows collection, the renderBlock will be evaluated instead of calling a selector. Let's use this to return a String instead of a Boolean. Change the WAColumnReport constructor for the "Can Buy" column to this:


(WAReportColumn
renderBlock: [:item | item isPurchasable asString]
title: 'Can Buy');


When the renderBlock message is called, the row data object is passed into the block (we're calling it "item" here). We call the isPurchasable message off the item which will return a True or False Boolean object. We then send that Boolean object the asString message which will return either "true" or "false". Save the initialization method, flip back to the browser, and refresh. You should be able to sort all the columns now.

That's it for this entry--make sure to save the want-tutorial.image. We're getting closer to having a working application. The next post will focus on creating and editing WantedItems by using an editor component.

Wanted -- A Seaside Tutorial: Part 3

In this entry we'll create our wanted item model object, handle storing instances of it, and also create a main page that will display a list of them. If you're not running Squeak, open it with the want-tutorial.image image file we've been saving our changes to.

Step 1: Create the Model
The Wanted model is pretty simple: a WantedItem object that contains 3 attributes:
  1. a title
  2. notes about the item
  3. the date the item was entered
With the Refactoring Browser open, click the Wanted-Tutorial package so that it is selected and the object creation template shows in the code pane. Replace NameOfSubclass with WantedItem. Inside the single quotes after instanceVariableNames:, enter title, notes and enteredDate. Save the new class definition. The class should appear in the class pane:


One of the nice things about the Refactoring Browser is that it can create accessor methods for instance variables. Double-click the title instance variable (inside the single quotes) so that it is highlighted, then right-click or option-click title and choose selection... then create accessors:


The Refactoring Browser should have created two additional methods in your WantedItem class: title and title:. These act as your getter and setter respectively. Whenever you see a colon in a message selector it means the message expects an argument--see the links in this post for help on understanding Smalltalk syntax. Notice that the message category accessing was created and that our accessors were put into it. Create accessors for notes and enteredDate and save. We now have an initial version of our WantedItem class that we can start using.

Step 2: Create an Object to Abstract Storage
One of the cool things about working with a Smalltalk image is that when you save the image, you save the state of all the objects in it. We can take advantage of this and use the image as a database by creating an object that will store our WantedItems as a collection. As long as you save the image each time before you exit it, all changes to the collection will be persisted. Once we get the application fleshed out and working the way we want, we'll move on to other types of storage.

The storage object is simple: it holds a collection of wanted items in a class instance variable. A class method will allow us to retrieve the collection and make changes to it.

Click the Wanted-Tutorial package so that it is selected and the object creation template shows in the code pane. Change NameOfSubclass to WantedDatabase and save. Click the class button in the class pane. The code pane should look like this:


Inside the single quotes after instanceVariableNames:, enter wantedItems:


and save.

Now we need to create an accessor. With the class button still selected, click on the "-- all -- " message category. Replace the method creation template in the code pane with the following:


wantedItems
^ wantedItems ifNil: [wantedItems := OrderedCollection new]


and save. Notice that the Refactoring Browser created a new message category "as yet unclassified". To keep things clean, right-click or option-click the "as yet unclassified" category, choose rename... and enter "accessing". We now have a very simple way of persisting our WantedItems: we ask the WantedDatabase for the wantedItems class instance variable by sending the wantedItems class message and add or make changes. Notice that if the variable has not been accessed before, a new empty OrderedCollection is created and assigned to the wantedItems variable before being returned.


Step 3: Create a Component for the Wanted List Page
Our main page in the Wanted application will display a list of saved WantedItems. From this page we will eventually provide links to add, delete, and edit WantedItems. Let's create a component to display the list.

In the Refactoring Browser, click the Wanted-Tutorial package so that the object creation template is displayed in the code pane. Change Object to WTComponent and NameOfSubclass to WantedList and save the component. Since WantedList is a subclass of WTComponent, we do not have to add the class canBeRoot method: it is inherited. We want WantedList to render differently than WTComponent, so click on the "-- all --" item in the message category pane so that the method creation template displays in the code pane. Create the renderContentOn: method by entering the following into the code pane:


renderContentOn: html
WantedDatabase wantedItems
do: [:wantedItem | html paragraph: wantedItem title]


and save:


Our renderContentOn: method asks the WantedDatabase for the wanted items collection, then for each wanted item we ask the html context to emit the title of the item in a paragraph tag. This is very simplistic, but for now allows us to view our stored wanted items.

Now flip back to your web browser and go to http://localhost:8080/seaside/config. Click on the configure link next to the wanted application entry point:


In the General section of the page, change the Root Component dropdown to WantedList:


and click the save button. After the page refreshes to notify you that the changes were saved, click the done button. This will take you back to the main config page. Click on the wanted link. You should see nothing since the only thing we are displaying are wanted items in paragraph tags, and we haven't created any. Let's create one.

Step 4: Create a WantedItem to Display
Flip back to Squeak, left click on the world, choose open...:


then Shout Workspace:


You should see a window like this one:


The workspace is kind of like a command line for your Smalltalk image with full access to all of the objects in it. You can evaluate code snippets, write scripts, start services, etc. The Shout Workspace adds some extra features like syntax coloring. We're going to use the workspace to add a WantedItem to our WantedDatabase. Enter the following code into the workspace:


WantedDatabase wantedItems add:
(WantedItem new
title: 'Nintendo Wii';
enteredDate: (DateAndTime now);
notes: 'Get some exercise while having fun')


On the first line we're asking the WantedDatabase for the wantedItems (which will be an empty OrderedCollection since this is the first time we're accessing it). We immediately send the collection the add: message, passing a new WantedItem as the argument. The wanted item is created inside the parentheses by passing the WantedItem class the new message, then setting the 3 instance variables. The semicolons are a Smalltalk language feature called the cascade operator that returns the receiver of the last message (again, see this post for links to understanding the Smalltalk syntax). Highlight the entire block of code, then right-click or option-click and choose "do it":



To make sure that the item was added, append the following block on its own line to the workspace:


WantedDatabase wantedItems size


Highlight it in the workspace, right-click or option-click, then select "print it". You should see the number 1 printed to the right of the block of code, letting us know that our item was added to the collection.

Now flip back to your browser and refresh. You should see the title of the item we just added in the workspace.

Congratulations--we're one step closer to world domination! Make sure to save your image (want-tutorial.image). Note that by saving the image, you'll be saving the Wii WantedItem we created. If you exit Squeak then fire it back up and go back to the wanted seaside page, it will show up again. Even cooler: install Squeak on another machine (even under a different operating system), copy the tutorial image to it, open the image and point your web browser to the seaside wanted app. You will see the same results. We now have a completely portable platform-agnostic development environment and database for our project.

In the next post, we'll look at tweaking the display of the WantedList component.

Wanted -- A Seaside Tutorial: Part 2

If you're following along from the last blog post, you should now have a working Squeak environment that has Seaside installed and preconfigured. I'm skipping over a lot of introductory Squeak material since there are plenty of resources out there. If you are new to Squeak, you may find it an odd, foreign, and sometimes frustrating experience initially. I certainly did, but the more I use it (and Smalltalk in general) the more amazed I am with it. Squeak is almost criminally passed over, with most people taking one look at the interface (especially images pre 3.9) and bolting. Stick around and give it a chance--there's a lot of power under that weird interface, and a huge learning opportunity for those with an open mind.

In this entry we will write the first Wanted Seaside component.

Step 1: Create the Component
A Seaside Component is an object that responds to user actions, holds state relating to those actions, and renders itself as html. A component may be responsible for rendering an entire page or act as an embeddable piece of html that another component includes. Components that answer the canBeRoot class message with a true value are given special treatment: they show up as application entry point components in the Seaside config screen and can be mapped to a url--we'll use the config screen later.

If you don't already have Squeak running with the wanted-tutorial.image we created in the last post, open it now. Right-click (Windows) or option-click (Mac) in the Packages pane (the first list view) in the Refactoring browser. You should see this menu:


Choose "add item..." and enter "Wanted-Tutorial" in the dialog box that pops up. This will create the package that we will put all our Wanted source in. The Refactoring Browser should show our new package and should have changed the code pane to an object creation template:




We want our object to be a Seaside component, so highlight the first word "Object" in the code pane by double-clicking it and change it to "WACom". Notice that the text you are typing is italicized and underlined. This is eCompletion doing a helpful service for you, searching out class objects that begin with the text you have entered. If you hit the tab key, eCompletion will display a matching list of two objects (WAComponent and WACompound) under your cursor. Make sure WAComponent is highlighted and press enter. Change "NameOfSubclass" to "WTComponent". This will be our ancestor that we will use for all of our Wanted components. Having a common ancestor allows us to reuse application-wide settings that we don't want to have to redefine in each component. Hit ctrl-s (Windows) or command-s (Mac) to save the object. Your Refactoring Browser should now look like this:



Step 2: Make the Component Available as an Application Root
Click on the class button in the Class pane (2nd list view) in the Refactoring Browser. Now click on "--all--" in the Message Category pane (3rd list view). The code pane now shows a default message template. Replace the text in the code pane with the following:


canBeRoot
^ true


What we've done is added a class-side message canBeRoot that returns a true value ("^" means return in Smalltalk). Save the message with a ctrl-s or command-s. Enter your initials in the dialog if you are prompted. Seaside will now know to treat this component as an application root entry point (we'll get to this in a bit).

Step 3: Have the Component Display Itself
Click the instance button, select the "--all--" message category, change the message template to the following:


renderContentOn: html
html text: 'Seaside Rocks!'


and save. The renderContentOn: message asks the component to display itself using a WAHtmlRenderer object. WAHtmlRenderer knows about html. It acts like a stream that you can keep appending html attributes to in order to build a whole or part of an html page. For now, we are telling the renderer to emit a simple text string.

Step 4: Wire the Component up to a Seaside URL
Now, point your web browser to http://localhost:8080/seaside/config. You should see the Seaside config page:

At the top of the page you will see the current Seaside version displayed. Underneath that is a list of applications that have entry points into Seaside. Underneath the entry points is a Settings section. In the Path text field, enter "wanted", leave the Type as Application, and click the Add button.

You should be taken to the wanted Application config page:



Click on the Root Component drop down. This list contains all the Seaside components that answer the canBeRoot message with a true value. For now, we want our wanted entry point url to be handled by the WTComponent object we just created, so choose WTComponent from the list and click the Save button. The application config screen should refresh to reflect the change we just made. Click the Done button. You should be taken back to the Seaside config screen which now contains a wanted application entry point:

Click the wanted link and you should see the "Seaside Rocks!" text we entered in our component's renderConentOn: message.

Congratulations--you've just created your first working Seaside application! Google's not going to acquire your startup just yet, but it's a first step. Think about what we just did: In more or less four steps and less than 10 lines of code we created a simple web application without editing a single xml file and without reloading the server to deploy our component. The only time we left the Squeak environment was to configure our component as an application entry point using the Seaside config utility.

That's the end of this entry. Left click on the world and choose save to persist the changes to the wanted-tutorial.image file. Next time we'll create the Wanted model and some more sophisticated components to manage some CRUD operations on the model.

Required Reading

Wanted -- A Seaside Tutorial

To kick off the new year, I thought I'd start writing a walkthrough of creating a simple Seaside application. Since I'm still pretty new to a lot of this stuff, the tutorial will be from a newbie discovering perspective. My hope is that it will be useful both for myself and for others new to Seaside. Each blog entry will reflect one step in the advancement of the application.

The application is called Wanted. Each time you find something that you think you want, you add it to your Wanted list. The Wanted list shows items that have one of two statuses: those that have passed a time threshold (say, two weeks) that are considered true wants, and those that have not. The theory is that you hold off on opening your wallet for a Wanted item until it has been around long enough that it is considered a true want. Hopefully in the time it takes for an item to pass the time threshold, you will have changed your mind about its importance, saving you some money. The Wanted application has a simple model (Wanted items) and a CRUD interface.


Step 1 -- Preparation
To get started, we need to have a Smalltalk implementation and Seaside installed. The Smalltalk implementation used for this tutorial is Squeak, but Seaside is also available for Cincom Smalltalk and Dolphin Smalltalk--consult the Seaside download page to see if your favorite implementation has a port. Using Smalltalk actually requires two things: a VM that executes Smalltalk code, and an image that stores the Smalltalk code the VM executes. You can find the latest release-ready Squeak VM + Image combination on the Squeak homepage in the upper right corner (as of this writing, the latest file is Squeak-3.9xxx.zip, where "xxx" is some OS-specific designation). Download the appropriate version for your OS and unpack it.

This tutorial will be using the Squeak.org VM, but not the default Squeak.org image. We're going to use Damien Cassou's squeak-web image found here. It contains Seaside, plus several developer niceties that the default image doesn't. Download the latest version (as of this writing, the latest file is "squeak-web-72.zip") and unpack it.

Once you have downloaded both files and unpacked them, copy the Squeak VM executable and the SqueakV39.sources file from the Squeak3.9 directory into the squeak-web directory:




Step 2 -- Up and Running
To run Squeak, click on the squeak-web-72.image file and drag it on top of the Squeak executable file. You should see something like this:



Step 3 -- A Couple of Tweaks
Collapse the two windows that are showing by clicking on the circle in the upper right corner of each window. Once the two windows are collapsed, you should see mostly white space. The white space is called the World. Left click on it to show the World menu. Click on "Open.." on the World menu, then click on "Refactoring Browser". This should pop this window up:


The Refactoring Browser allows you to see and edit all the source stored in the currently running image. Running along the top of the browser window are four list views.

The first list contains the image's Packages. Packages provide a way to group related classes together. Click on the Kernel-Numbers package and the second list view will display the classes in that package. Click on the Complex class and the third view will display the Complex class Categories. Categories provide a way to group related messages the class/object responds to. The fourth list displays those messages. By default, when clicking on a class, all messages are displayed. Clicking on a specific category allows you to filter the messages. Select the "arithmetic" category, then select the "abs" message. The bottom pane should show the source code for the abs message. This pane is where you will change the world.

Notice the three buttons in the bottom of the Class list view (the 2nd list): instance, ?, and class. By default, the instance button should be selected. The instance and class buttons toggle which messages are displayed: instance messages (messages that a class's object instance responds to), and class messages (those that the class itself responds to). The Complex class responds to 3 messages, all of which create a Complex instance object: abs:arg:, new, and real:imaginary:.

Step 4 -- Saving
At this point, left click on the World and choose "save as...". In the "New File Name?" dialog, enter "want-tutorial.image" and click the Accept button. If you look in the squeak-web-72 directory, you should see the new image we created. This is where we will store all code related to this tutorial. Notice we didn't save over the squeak-web-72.image file--this was left intentionally as a starting point for other projects.

We have made three changes to the want-tutorial.image that will persist for the next time we run: The two collapsed windows that were open, and the opened Refactoring Browser. That's it for this entry. Feel free to look around--you can't really mess anything up as long as you don't choose one of the "save" options from the World menu before exiting. We'll start looking at Seaside in the next entry.

Useful links:
Squeak.org
Introductions to Squeak