Seaside Tutorial
Software Architecture Group

10 - Magritte

What you are going to learn

What is Magritte?

In this chapter, we will introduce Magritte, a meta-description framework. You should have read and understand the other chapters before you continue reading.

As you remember, we have created a special dialog (StTaskEditor) for creating and editing tasks. What if we do not only have tasks but also many other different objects that should be editable via such a dialog? And what if we have multiple lists - do we always need to create a list view like StListComponent? The answer is: no, we use Magritte!

Magritte provides a set of so-called descriptions. You use them to declaratively describe all attributes of a domain object. Such a description defines the type of each attribute, for example, a Date, Boolean, or any other object. By default, all descriptions are collected into a description container. In addition, you can also use dynamic descriptions, that means you define new description containers with a subset of the object's descriptions.

These containers are used when creating a component for an object. This is done by sending #asComponent to that object. For every description stored in the container, an input field will be displayed and filled with the current value of the described attribute. You see: not only can Magritte create a task editor for us, it is even more powerful than ours!

Magritte was invented by Lukas Renggli in his master's thesis. As a proof of concept, he implemented the content management and wiki system Pier. Both are still improved and used by many Smalltalkers around the world.

So let us start rewriting our ToDo Application.

Installation

If you used a prepared image, e.g. the Seaside All-In-One image, the necessary Magritte packages are already installed. Otherwise you need to install the packages manually.

We use the Metacello Configuration of Magritte to load all packages. To do this, execute (Do-It) the following code snippet in a Squeak Workspace:

Installer squeaksource
 
project: 'MetacelloRepository';
 
install: 'ConfigurationOfMagritte2'.
(Smalltalk at: #ConfigurationOfMagritte2) project latestVersion load: 'Magritte-Seaside'.

Describe StTask

In our ToDo Application, we have two domain objects: the user and the task. We want to use Magritte for the creation of input dialogs for the tasks. Hence, we have to add descriptions for every instance variable that should be editable. They are as follows:

completed
for Boolean values
deadline
for a Date
taskDescription
for multi-lined text
taskName
for a single-lined text

Let us start with the description for completed:

descriptionCompleted

 
^ MABooleanDescription new
 
 
selectorAccessor: #completed;
 
 
label: 'Completed?';
 
 
priority: 30;
 
 
default: false;
 
 
yourself
StTask class>>#descriptionCompleted

What can we learn from this code? The method name starts with description. As said above, Magritte queries all these methods. The second part of the method name is, by convention, the name of the described instance variable. The method returns an instance of MABooleanDescription. For many data types there are predefined description classes as we will see in the following.

Every description has the methods

(there are many others; see MADescription). #SelectorAccessor: gets the name of the accessor method as a symbol. In our case, the StTask has the setter #completed: and the getter #completed. #Label defines a string that is shown in front of the input element to identify the field. The value of #priority: determines the order of the elements in the generated view. The first element is the one with the lowest value, the last the one with the highest. The #default message sets the default value for that element.

Now, we can create the descriptions for deadline and taskDescription:

descriptionDeadline
 

 
^ MADateDescription new
 
 
selectorAccessor: #deadline;
 
 
label: 'Deadline';
 
 
priority: 20;
 
 
default: Date today;
 
 
yourself
StTask class>>#descriptionDeadline
descriptionTaskDescription
 

 
^ MAMemoDescription new
 
 
selectorAccessor: #taskDescription;
 
 
label: 'Description';
 
 
priority: 40;
 
 
default: '';
 
 
yourself
StTask class>>#descriptionTaskDescription

We see that the description class for date input is MADateDescription, the one for multilined texts MAMemoDescription. Now, we will create the description for the task name including the conditions for a valid name. Conditions can be added using the #addCondition: labelled: method which accepts a block as argument. The block can have one argument that represents the current value of the instance variable. If the block evaluates to false, then the given message is displayed.

descriptionTaskName
 

 
^ MAStringDescription new
 
 
selectorAccessor: #taskName;
 
 
label: 'Task Name';
 
 
priority: 10;
 
 
default: 'New Task';
 
 
addCondition: [:value | (value ~= 'New Task')
 
 
 
and: [value size <= 50]]
 
 
labelled: 'Task name is invalid or too long';
 
 
yourself
StTask class>>#descriptionTaskName

Create a Magritte Task

After having created the Magritte descriptions for a StTask, we want to see it in action in the next step. New tasks are created using the StTaskEditor which was called in message StLoggedInComponent>>#createNewTask. So let us rewrite its source code:

createNewTask
 

 
| task |
 
task := self call: StTask new asComponent addValidatedForm.
 
task ifNotNil: [self session database
 
 
addTask: task toUser: self session user].
StLoggedInComponent>>#createNewTask

Now, log in to the ToDo Application and create a new task. The form will look as shown in figure 10.1.

Figure 10.1: Task Editor generated by Magritte

How does this work? Let us have a look on the code again. We have created a new StTask and called #asComponent. This method is provided by Magritte and implemented in Object. It calls the #description* methods and creates a component with input fields for all descriptions. By default, the created dialog does not have any submit buttons. Only when sending the #addForm or #addValidatedForm message, buttons are added. When the cancel button is pressed, the component answers nil. If the save button was pressed and all conditions from the descriptions evaluate to true, the created (or edited) object is returned. In case of unfulfilled conditions, the form is displayed again and the error message is shown. The latter is only done if the component received the #addValidatedForm message.

If the component returned a task, then we store it in the database.

Dynamic Descriptions

In the last step, we used Magritte for creating input forms for new tasks. In this step, we want to use it for editing existing ones. It is quite similar. But when editing a task, we also want to display the id of the task without it being editable. Therefore, we create a dynamic description. On the instance side of StTask, we add #descriptionEdit. The name of the description starts with description and is followed by some identifying words by convention. It is not necessary like at class side, but it is a good idea to stick to the naming schema.

descriptionEdit
 
 

 
^ self description copy
 
 
add: (MANumberDescription new
 
 
 
selectorAccessor: #id;
 
 
 
label: 'Id';
 
 
 
priority: 5;
 
 
 
beReadonly;
 
 
 
yourself);
 
 
yourself
StTask>>#descriptionEdit

We copy the default description container and add a new description for a number as the id is a numerical value if set. By sending the #beReadonly message, the created input element will not be editable by the user.

The container must be copied as it is cached by Magritte. Unless we copy it, we would always add another MANumberDescription every time the method is called.

You cannot only add descriptions to existing containers, you may also want to omit descriptions. Therefore, the descriptions container implements the whole Collection protocol, that means methods like #select: or #collect:.

Back to our ToDo Application. Now, we have a dynamic description that should be used for an edit dialog. So, StLoggedInComponent>>#editTask: has to be modified.

editTask: aTask
 

 
| task |
 
task := self call: (aTask descriptionEdit asComponentOn: aTask) addValidatedForm.
 
task ifNotNil: [self session database save: aTask].
StLoggedInComponent>>#editTask:

The method looks quite similar to #createTask. They differ a bit in saving the modified task and a bit more in the way how Magritte is involved. For an empty object, for object creation so to speak, we called StTask new addValidatedForm. But for existing objects, we request the description container which should be shown (aTask descriptionEdit in our case) and call the message #asComponentOn: followed by the object to edit. This will create the component to be shown.

Replace one aTask by StTask new in the first line and see what is happening. If you replace the first one, nothing channges. That is obvious because #descriptionEdit returns the same description container for both an empty and an existing task. But as the second aTask is the object to modify, you will see an empty dialog when replacing it by StTask new. If you call #description instead of #descriptionEdit, you will get the dialog you already know from creating tasks. But it is also filled with the values of the edited task.

Do you remember how much code was required to create the StTaskEditor component? It does the same things we just did with Magritte. Imagine you do not only have one or two views on an object but a dozen. Using Magritte can save you many lines of code.

Create a Report

Magritte is not only able to create input forms for objects. It can also display reports, a table of a collection of objects. In this section, we want to create such a report and use it instead of StListComponent. The list should also simply display the name and the deadline of the task and wether it is completed. A report is created by calling MAReport class>>#rows:description:. The first argument is the collection of items to display and the second a description container that describes each item. We just want to show the name and the deadline of the task, so we have to create a second dynamic description:

descriptionReport
 

 
^ super description
 
 
select: [:each | #(taskName deadline) includes: each accessor selector]
StTask>>#descriptionReport

Now we can create the report. As it is a component, too, we need an instance variable for it to be stored in which must also be returned by StLoggedInComponent>>#children. So, create an instance variable called report with corresponding accessors, and modify #children to look like this:

children
 

 
^ Array with: self menuComponent with: self report
StLoggedInComponent>>#children

The report is built once and updated every time items are added, modified or the selection is changed. Let us create a method StLoggedInComponent>>#initializeReport that initializes the initial report.

initializeReport
 

 
self report: (MAReport
 
 
rows: self session user tasks
 
 
description: StTask new descriptionReport).
 
self report
 
 
addColumn: (MAToggleColumn new
 
 
 
accessor: #completed;
 
 
 
label: 'Completed';
 
 
 
description: StTask descriptionCompleted;
 
 
 
yourself);
 
 
addColumn: (MACommandColumn new
 
 
 
addCommandOn: self selector: #editTask: text: 'edit';
 
 
 
yourself).
StLoggedInComponent>>#initializeReport

As described above, the report is created by calling MAReport>>#row:description:. Initially, we add all our tasks to the report. The description is the newly created dynamic one, queried from an empty task.

In the second part of the message, we add two columns to the report. The first one contains a link to toggle the completion state of the task, the second holds a link to edit the task. Let us have a closer look at both. The MAToggleColumn needs the selector of the instance variable to toggle. The column needs a description to know what type of value should be displayed, so we use the StTask class>>#descriptionCompleted message we have already defined. The edit link is even easier to create. We add a MACommandColumn to the report and call addCommandOn: anObject selector: aSymbol text: aString. This creates a link with the caption aString. When clicking on the link, aSymbol is called on anObject and the item displayed in the current row is supplied as argument. In our case, we call the #editTask: message of self (StLoggedInComponent). Pay attention on the colon at the end of #editTask:, it is part of the selector's name.

Since we want to filter tasks out of the list (for example the pending ones when displaying the completed), we need another instance variable with accessors, namely filterBlock, that keeps a block. It will be evaluated for every task and only those tasks are displayed, that evaluate to true.

The #initializeReport message must be called when initializing the component. So we change StLoggedInComponent>>#initialize to look like this:

initialize
 

 
super initialize.
 
self
 
 
initializeMenuComponent;
 
 
initializeReport;
 
 
filterBlock: [:item | true].
StLoggedInComponent>>#initalize

The next step is to render the report instead of the list component. To achieve this, we have to modify StLoggedInComponent>>#renderContentOn:. Instead of calling self listComponent in the last line we call self report. That is it. Start a new session of your ToDo Application and have a look at the report Magritte rendered for us.

You may have seen very quickly that we still have a little work to do. The links to limit the tasks do not work yet and the report will not be updated when adding or editing some tasks. So let us fix this right now.

Similar to the list component, we use a filter block to filter all unneeded items. Create an instance variable filterBlock and corresponding accessor methods. The refresh of the report is done in a method #refreshReport.

refreshReport
 

 
self report rows: (self session user tasks select: self filterBlock).
StLoggedInComponent>>#refreshReport

As you see, we set the rows of the report to all the user's tasks that match the filter block. Now we change all the show methods:

showAllTasks
 

 
self
 
 
filterBlock: [:item | true];
 
 
refreshReport.
StLoggedInComponent>>#showAllTasks
showCompletedTasks
 

 
self
 
 
filterBlock: [:item | item completed];
 
 
refreshReport.
StLoggedInComponent>>#showCompletedTasks
showMissedTasks
 

 
self
 
 
filterBlock: [:item | item hasBeenMissed];
 
 
refreshReport.
StLoggedInComponent>>#showMissedTasks
showPendingTasks
 

 
self
 
 
filterBlock: [:item | item isPending];
 
 
refreshReport.
StLoggedInComponent>>#showPendingTasks
createNewTask
 

 
| task |
 
task := self call: StTask new asComponent addValidatedForm.
 
task ifNotNil: [self session database
 
 
addTask: task toUser: self session user.
 
 
self refreshReport].
StLoggedInComponent>>#createNewTask
editTask: aTask
 

 
| task |
 
task := self call: (aTask descriptionEdit asComponentOn: aTask) addValidatedForm.
 
task ifNotNil: [self session database save: aTask.
 
 
self refreshReport].
StLoggedInComponent>>#editTask:
Figure 10.2: The report created by Magritte

If a task was added or edited, the report must also be updated by calling #refreshReport.

That is all we need to do. We still have the same application (see figure 10.2, it looks like the list component), but used less code to implement it. All dialogs and reports we have had to create by ourselves are now generated by Magritte. All we had to do was to describe the attributes of our domain objects, StTask in this example.

Summary

In this chapter, you have learned how to use Magritte to create powerful input dialogs and reports. The only thing you had to do was to describe your domain objects properly and then to invoke Magritte. Magritte generates the dialogs and validates your given conditions. With Magritte, you can develop faster and save a huge amount of code.

Magritte also prevents your objects to become invalid during editing by the use of the Memento pattern: the input dialog is working on a copy of the original object. Only when you successfully submit the form and no condition fails its data are copied back. If another person changed the same object concurrently, Magritte tries to merge your changes automatically.

You cannot only describe basic data types like numbers or strings. Complex structures, that are classes or even collections of classes, can also be described using relation descriptions. Of course, you can extend the description system by your own descriptions, too.

Magritte is not limited to Web applications. There is a Morphic binding, too, that works quite similarly. Instead of sending #asComponent, you call #asMorph and a new morph is shown. If you have described your domain objects once, you can use these descriptions in both environments.

The layout of the generated forms is not finite. You can use CSS to style it, but in some cases you may want to have a very special layout. Therefore, you can create your own Magritte renderer or component. Within domain objects, you can then define that your renderer or component should be used instead of the default one.

This chapter was only a very small introduction to the meta-description framework Magritte. We hope that you have seen that it allows you to fast and easily create different views to your domain objects, faithfully obeying Magritte's motto: Describe once, get everywhere.