10 - 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.
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:
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:
Let us start with the description for completed:
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:
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.
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:
Now, log in to the ToDo Application and create a new task. The form will look as shown in figure 10.1.
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.
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.
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.
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.
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:
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:
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.
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:
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.
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:
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.
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.