Showing posts with label seaside. Show all posts
Showing posts with label seaside. Show all posts

Wanted: Migrating To a Newer Image Part 2

In the last post we migrated the Wanted source code to a newer image by using a local Monticello repository. That gave us a working Wanted application, but the WantedItems that we created in the old image weren't copied over. This post will focus on copying the data out of the old image and into the new one.

Write Out the Collection of WantedItems

Fire up the old Squeak image, open a workspace and execute the following:

| out |
out := DataStream newFileNamed:
'/path-to-your-directory-here/wanted-items.dat'.
out nextPutAll: WantedDatabase wantedItems.
out close

This creates the temp variable out to hold a DataStream object pointing to a new "wanted-items.dat" file. The nextPutAll method takes a collection of objects and writes it to the receiving stream before closing it.

Exit the old Squeak image--this will be the last time we need it. From now on, all things in these tutorials will be done in the new image.

Read In the Collection of WantedItems
Fire up the shiny new image, open a new workspace, and execute the following:

|in|
in := DataStream fileNamed:
'/path-to-your-directory-here/wanted-items.dat'.
[in atEnd] whileFalse:
[WantedDatabase wantedItems add: in next].
in close

The above creates a new DataStream off of the "wanted-items.dat" file (notice we're using the fileNamed message to create the DataStream instead of newFileNamed). The [in atEnd] block will return false until all objects have been read from the file. We pass that block another block that adds each object read in (using the next message) to the WantedDatabase wantedItems collection. Then we close the DataStream.

We now have our old WantedItems in the new image. Open up a browser and head to http://localhost:8080/seaside/wanted. You should see something like this:



Enable File Serving in Commanche

Our new image is not serving our css since Commanche is not enabled to serve files. Paste the following into a new workspace and "do it":

| ma seaside |
HttpService allInstancesDo:
[:each | each stop. each unregister].
WAKom stop.
seaside := WAKom default.
ma := ModuleAssembly core.
ma serverRoot: FileDirectory default fullName.
ma alias: '/seaside' to:
[ma addPlug: [:request | seaside process: request]].
ma documentRoot: FileDirectory default fullName.
ma directoryIndex: 'index.html index.htm'.
ma serveFiles.
(HttpService startOn: 8080 named: 'httpd') plug: ma rootModule

That should get you to this:


That Entered Date Format Has Been Bugging Me
Me too. We can solve that by adding a formatBlock to the WAReportColumn that handles the enteredDate in WantedList>>initialize:

add:
((WAReportColumn selector: #enteredDate title: 'Entered Date')
formatBlock:
[:enteredDate | enteredDate asDate mmddyyyy];
yourself);

We get the date to show up in mm/dd/yyyy format by sending the asDate message to the enteredDate of the WantedItem, then sending the mmddyyyy message to that date.

While we're there, let's modify the "Can Buy" WAReportColumn to show 'Yes' or 'No' instead of the robot-like 'true' or 'false':

add:
(WAReportColumn
renderBlock:
[:item | item isPurchasable
ifTrue: ['Yes'] ifFalse: ['No']]
title: 'Can Buy');

This is accomplished by sending the isPurchasable predicate the ifTrue:ifFalse: message with a "Yes" or "No" value respectively.

Those changes should give you something like this:


Saving Changes
Since we made source changes, we should commit them to our local Monticello repository. Open a Monticello Browser and select the Wanted-Item package in the left pane, and the local repository in the right pane:

Notice that an asterisk appears before the package name: this signifies that changes have occured in the package. Click the Save button and enter a version message describing the changes we made:


When you hit Accept, a version info window will display letting you know that the changes were successfully saved. At this point, save the image so that the WantedItems will be persisted. And that's it for this post--we are now fully migrated over to the new image. By now it feels like we've kinda worn out the simplicity of saving the WantedItems in a collection in the image. If we had had the data in a store somewhere, we could have just pointed the new image there instead of this file reading/writing stuff. Future posts will address this.

Wanted: Migrating to a Newer Image

Damien Cassou recently announced the release of a new Squeak-Web image which can be found here. This post will focus on migrating the Wanted application from the image we've been using to his new one. Before this post, I was keeping my squeak stuff spread out all over the place, and since I'm migrating to a newer image I thought I'd get a little more organized and detail my setup here.

A More Disciplined Directory Structure
Instead of scattering everything all over the place, I created a proj directory with the following directory tree:


  • proj/squeak is where all Squeak-related stuff goes
  • each directory in apps contains a combination of an image plus a vm
  • doc is for all Squeak/Smalltalk related pdfs like free books
  • images and vms contain different versions of squeak images and vms
  • repository is for the local Monticello repository, which we'll get to in a bit
  • tmp should be obvious
The "apps/wanted" directory is a new directory created for the new squeak-web image. My old image that I've been using for the Wanted tutorial is sitting in a different location.

Setting Up a Local Monticello Repository
We're going to use Monticello to handle the versioning of Wanted (there's some overview info on Monticello here). Fire up the image you've been using for the Wanted tutorial, then click on the World and select open... -> Monticello Browser. You should see this window:


Click the "+Package" button. Enter "Wanted-Tutorial" (or the name of the package containing the Wanted tutorial objects in your image) in the "Name of package" dialog:


and click the Accept button. Then add a local repository by clicking the "+Repository" button, and choosing "directory" from the "Repository type" dialog:


The "Repository folder" dialog will then display:


Navigate to the folder you want to store the local Monticello repository in and hit the "ok" button. You should now be looking at the Monticello browser with the Wanted-Tutorial package selected in the left pane and the repository folder selected in the right pane. Click the "Save" button in the row of buttons in the Monticello browser. Enter you initials in the dialog that pops up and Accept. The "Edit Version Name and Message" dialog will pop up:


Enter a comment and Accept. If all went well you should end up with a window like this:


We now have a working versioning repository for our Wanted tutorial code. As you'll see, migrating it to newer images will be a snap. At this point, close the old image but keep it around--we'll need it later.

Setting Up the New Image
After downloading Damien's new image and unpacking it into the "images/squeak-web-95-2" directory (see the directory layout above), I copied the
  1. squeak-web-95-2.image
  2. squeak-web-95-2.changes
files into the "apps/wanted" directory. I then copied the
  1. Squeak3.8.xxx
  2. SqueakV39.sources
files from the current vm in the "vms" directory into the "apps/wanted" directory. I also copied the
  1. wanted.css
file from the old image directory into "apps/wanted". Once all 5 files are copied, in "apps/wanted", drag the squeak-web-95-2.image file over the Squeak executable to open Squeak with the new image. Collapse the Script Manager and Preference Browser windows to make some room.

Open a Monticello Browser and click the "+Repository" button. Choose "directory" from the "Repository type" and select the directory you created above. With the directory repository highlighted in the right pane in the Monticello Browser, click the "Open" button. You should see a window like this:

Highlight the "Wanted-Tutorial...1.mcz" file in the right pane (the version information will appear in the bottom pane when you do so), then click the "Load" button. After the progress dialogs go away, open a new Refactoring Browser and scroll to the bottom of the Packages list:

Well looky there! I'll wait for you if you want to get up and do a happy dance.

In this new image, Seaside doesn't know about our Wanted app yet because we haven't registered it. If you open a web browser to http://localhost:8080/seaside/wanted you will get a listing of the registered Seaside apps, and Wanted isn't one of them. Let's fix that. Open a Shout Workspace, enter the following, highlight and "do it":


WantedList registerAsApplication: 'wanted'


Now go to http://localhost:8080/seaside/wanted. Notice that our app shows up, but all of our WantedItems are missing! Stay tuned--we'll fix that in the next post. For now, save the new image. I'm just going to use the default "squeak-web-95-2.image" name instead of naming it "wanted.image" since it's already in the "wanted" directory. If you wanted you could name it "why-is-sanjaya-not-voted-off-yet.image" and the world would still turn and make bad decisions and watch bad TV.

Wanted: Beautification is Easy

This post will focus on adding style to Wanted with some very simple changes.

Step 1: Enable File Serving in Commanche
If you've been following the Wanted tutorials and are using the squeak-web image, you'll have to restart the Commanche server and enable file serving. Enter the following in a new workspace, highlight it, and "do it":

| ma seaside |
WAKomEncoded39 stop.
seaside := WAKomEncoded39 default.
ma := ModuleAssembly core.
ma serverRoot: FileDirectory default fullName.
ma alias: '/seaside' to: [ma addPlug: [:request | seaside process: request]].
ma documentRoot: FileDirectory default fullName.
ma directoryIndex: 'index.html index.htm'.
ma serveFiles.
(HttpService startOn: 8080 named: 'httpd') plug: ma rootModule
See David Shaffer's Seaside tutorial for more details.

This will stop Commanche and restart it with support for serving files and Seaside applications. Note that it sets the document root to FileDirectory default. If you are starting Squeak by dragging the image file onto the Squeak executable, this should be the directory of the Squeak executable. You can find out exactly what it is by entering "FileDirectory default fullName" in a workspace, highlighting it, then doing a "Print It".

Test it by flipping to your browser and going to http://localhost:8080. You should see a directory listing of the default directory.

Step 2: Create a CSS File
Create a new text file called "wanted.css" in the default directory that contains the following:

body {
margin:50px 0px; padding:0px;
text-align:center;
font: 12px arial, sans;
}

table {
margin-left: auto;
margin-right: auto;
border: 1px solid #848370;
border-collapse: collapse;
margin-bottom: 20px;
}

th {
background-color: #b4bfcc;
padding: 4px;
font-size: 14px;
}

td {
padding: 4px;
}

a {
color: #103b61;
text-decoration: none;
}

a:hover {
text-decoration: underline;
}

form {
margin-left: auto;
margin-right: auto;
width: 400px;
text-align: left;
background-color: #dee3e4;
border: 1px #ccc solid;
padding: 10px;
}


form input, textarea {
border: 1px solid #b4bfcc;
padding: 2px;
width: 98%;
margin-bottom: 10px;
}


form textarea {
height: 150px;
font: 12px arial, sans;
}

form input.submit {
margin: 0;
width: 50%;
}

Step 3: Wire the CSS File Up to the Components
To link to the css file, we'll implement updateRoot in WTComponent:

updateRoot: anHtmlRoot
super updateRoot: anHtmlRoot.
anHtmlRoot linkToStyle: '/wanted.css'
Since both of our view components (WantedList and WantedEditor) inherit from WTComponent, they will both contain the link to the css file.

Flip back to the browser and refresh:

Much better, huh? The application looks completely different and we didn't have to touch any rendering code or a single line of html. I'll be the first to admit that my design skills aren't that inspiring--if someone comes up with something better they'd like to share, I'll post it here.

Step 4: Tweak the WATableReport Row Colors
If you create more than 3 wanted items, you will see that yellowish background appear. That is WATableReport doing some styling for us. If we wanted to change it so that every other line was shaded with a light gray color, we would edit WantedList>>intialize by finding where the wantedReport instance is being assigned and changing it to the following:

wantedReport := WATableReport new rows: rows;
columns: columns;
yourself.
wantedReport rowPeriod: 1.
wantedReport rowColors: {'#ffffff'. '#eee'}

See David Shaffer's WATableReport tutorial for more about styling the table report object. After clicking the "New Session" link, you should see the following:



Conclusion
Making Wanted look decent was pretty simple using an external css file. Since Wanted really only consists of one table component and one form component, I was able to get away with not using divs, classes or ids. In an application with more depth these would be required.

Wanted -- A Seaside Tutorial: Part 5

In the last post we created a constructor for the WantedItem class that made it easier to add WantedItems to the WantedDatabase in a workspace. In this entry we'll create a WantedEditor component that will allow us to create and edit WantedItems using the web app. Grab your beverage of choice and fire up Squeak with the want-tutorial.image.

Step 1: Refactor the WantedItem Class
One of the things that the WantedItem title:notes: constructor does is initialize the enteredDate instance variable. If we create a WantedItem using the new message, the enteredDate will not get set. Let's change WantedItem to guarantee that any new WantedItem instance automatically gets an enteredDate. We'll do that by adding an initialize method to WantedItem:

initialize
super initialize.
enteredDate := DateAndTime now

Now that the initialize method handles initializing the enteredDate, we can take it out of the title:notes: constructor on the class side:

title: aTitle notes: someNotes
^ self new title: aTitle;
notes: someNotes;
yourself


Step 2: Create the WantedEditor Class

Create a new class called WantedEditor that subclasses WTComponent and add a wantedItem instance variable:


Highlight the wantedItem instance variable, then right-click or option-click it and choose "selection...", then "create accessors". Change the wantedItem getter to the following:

wantedItem
^ wantedItem
ifNil: [wantedItem := WantedItem new]

The purpose of WantedEditor is to either create a new WantedItem instance, or edit an exising one. Having a getter that lazy initializes wantedItem to a new WantedItem instance if it doesn't already exist allows for creation. Having a setter that allows us to set wantedItem to an existing instance allows for editing. We'll get to this in a bit.

Call And Answer
Before we continue finishing the WantedEditor component, we need to learn how WantedList will be able to transfer control from itself to an instance of WantedEditor. Seaside provides the call and answer messages for this. We'll have WantedList send itself the call: message with an instance of a WantedEditor as the argument. Control of the application will then switch to the WantedEditor instance which will display its interface and wait for user action. When WantedEditor is done with its work, it can revert control back to WantedList by sending itself the answer: message. The answer: message accepts an argument that is returned back to the site of the call:. This way a WantedList can ask a WantedEditor to create a WantedItem and return it back to the WantedList with the answer: message:


We're going to work a little backwards by implementing answer: before call: since we're already in WantedEditor. Let's start with creating WantedEditor's renderContentOn: method.

renderContentOn: html
html
form: [html text: 'Title: '.
html textInput on: #title of: self wantedItem.
html break.
html text: 'Notes: '.
html textArea on: #notes of: self wantedItem.
html break.
html submitButton on: #save of: self.
html cancelButton on: #cancel of: self]

This will produce the following form:

The WantedEditor renders an html form that contains text form elements for title and notes and two submit buttons for saving and cancelling. Each html element is created by asking the html canvas for the control it wants, then sending the control the on:of: message.

The on:of: messages wire each form element to a selector (the on argument) of an object (the of argument) when the form is submitted. For the buttons, this means sending the WantedEditor instance the save or cancel message. For the text elements, this means sending the value in the form element to the selector (title and notes) of "self wantedItem". Remember, if a wantedItem hasn't been set, a new instance of WantedItem will be created in the first call to "self wantedItem".

We need to create the two methods that correspond to the selectors used for the buttons:

cancel
self answer: nil

and:

save
self answer: wantedItem

Notice that both methods end by sending the WantedEditor instance (self) the answer: message. Both return an object back to the calling site: cancel returns a nil object since nothing has changed or been created, and save returns the wantedItem we were editing.

Now we need to wire up WantedList to WantedEditor.

Step 3: Call WantedEditor to Add WantedItems
Back in WantedList, add a link to add new wanted items in renderContentOn:

renderContentOn: html
html render: wantedReport.
html anchor on: #add of: self

When a user clicks the link, the add message will be sent to self (the WantedList instance). Let's go ahead and create add:

add
| wantedItem |
wantedItem := self call: WantedEditor new.
wantedItem
ifNotNil: [WantedDatabase wantedItems add: wantedItem]

We start by creating a wantedItem temporary variable. We then send the WantedList instance the call: message with a new instance of WantedEditor. The WantedEditor will assume control of the web application and display the user the form that allows them to set the wantedItem instance attributes. The first call to "self wantedItem" will lazy initialize a new wantedItem instance (which will now have the enteredDate set). If the user clicks the Cancel button a nil object will be returned (answered). If the user clicks the Save button, the wantedItem instance they were editing will be returned (answered). The result of either cancel or save is assigned to our wantedItem temporary variable. If it is not nil (they pressed the Save button), we add it to the WantedDatabase wantedItems collection.

Let's add a new WantedItem to make sure it works. Open a web browser to http://localhost:8080/seaside/wanted. Click the "Add" link, fill in the form, then click the "Save" button. You should be returned to the wanted list which now contains the new wanted item at the bottom. Woohoo!

Step 4: Call WantedEditor to Edit WantedItems
To let the user select a wanted item to edit, we'll turn the Titles of the wanted items into links that when clicked call a WantedEditor instance:


To turn the Title text into links, we'll need to revisit the initialize method of WantedList where we created the WATableReport. Change the line where the title WAReportColumn is being created by adding a clickBlock and returning yourself:

initialize
| rows columns |
super initialize.
columns := OrderedCollection new
add: ((WAReportColumn selector: #title title: 'Title')
clickBlock: [:item | self edit: item];
yourself);

add: (WAReportColumn selector: #enteredDate
title: 'Entered Date');

add: (WAReportColumn
renderBlock: [:item | item isPurchasable asString]
title: 'Can Buy');
yourself.
rows := WantedDatabase wantedItems.
wantedReport := WATableReport new rows: rows;
columns: columns;
yourself

The clickBlock will receive the WantedItem that was clicked on (we name the block argument "item"). The block sends the edit message with the item to self (the WantedList instance). We need to create the edit method:

edit: aWantedItem
self call: (WantedEditor new wantedItem: aWantedItem;
yourself)

Here we are calling a new WantedEditor instance again, but this time we are setting the wantedItem instance instead of having a new one lazy initialized. When WantedEditor's renderContentOn: method is called, it will use the values from the wantedItem object we passed to it to fill in the form values.

Go back to the browser and click on the "New Session" link at the bottom of the page. We have to do this since our WantedList instance has already been initialized--clicking the "New Session" link will force a new WantedList instance to intialize, creating a new wantedReport that has the clickBlock changes we made. Click on one of the title links and you should get the wanted editor page. If you edit the title and click Save you will be taken back to the wanted list page and the title for that wanted item should have changed in the wanted list.

We're almost there: the last piece of functionality is to remove WantedItems.

Step 5: Add Remove Functionality
To add remove functionality, we're going to add a "Remove" link for each wanted item row. To do this, we'll have to add another column to our wantedReport in the WantedList>>initialize method:

initialize
| rows columns |
super initialize.
columns := OrderedCollection new
add: ((WAReportColumn selector: #title title: 'Title')
clickBlock: [:item | self edit: item];
yourself);

add: (WAReportColumn selector: #enteredDate
title: 'Entered Date');

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

add: (WAReportColumn new title: 'Remove';
valueBlock: [:item | 'Remove'];
clickBlock: [:item | self remove: item];
yourself);
yourself.

rows := WantedDatabase wantedItems.
wantedReport := WATableReport new rows: rows;
columns: columns;
yourself

We set the title to "Remove", set the value to render (the link text) to "Remove", and the action to take when clicked to sending self the remove message.

Let's add the remove: method:

remove: aWantedItem
WantedDatabase wantedItems remove: aWantedItem

This simply removes the WantedItem instance from the WantedDatabase wantedItems collection.

Flip back to the browser and click the "New Session" link. You should see a "Remove" link next to each wanted item row. Go ahead and click on of them. The item should be removed from the WantedDatabase, then the page should re-render with the item removed from the table.

And that's it! Go forth and buy less!

Conclusion
At a basic level, this tutorial is complete. We have a first cut at a working application: we can Create, Report, Update and Delete WantedItems. The posts so far have shown how to build a simple application using the basic building blocks of Seaside. The app is by no means finished: it's ugly, it's not using the latest and greatest ideas and tools available for seaside (things like Magritte and Scriptaculous), there aren't any unit tests, we're using the image to store our items , we haven't provided a way to import or export the data, etc. Future posts will focus on some of these concerns, but may not use the Wanted material. Stay tuned!

Update
There was a bug in WantedEditor where pressing the Cancel button would persist any changes made to the title or notes fields. To change this, the Cancel "submitButton" has been changed to a "cancelButton". If you are using the same squeak-web image version downloaded in the first part of this tutorial or you do not have WARenderCanvas>>cancelButton, you will need to upgrade the Seaside version. Here's how to do it step by step:

  1. Left-click on the world and choose "open..." then "Monticello Browser".

  2. Click on the "Seaside2" entry in the left list pane.

  3. Click on the "http://www.squeaksource.com/Seaside" url entry on the right list pane so that it is highlighted.

  4. Click the Open button above the right list pane.

  5. Find and click on the entry marked "Seaside2.7a1-mb.142.mcz" in the right list pane so that it is highlighted. This was the commit that added the cancelButton functionality.

  6. Click the Load button above the right list pane.

  7. Close the Monticello windows and change the Cancel submitButton to a cancelButton in WantedEditor>>renderContentOn:.


I apologize for not finding this earlier.

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