The real app#
We leave our scaffold app behind in the "basic" project, and make a fork into the "category" project - this is held as a new SVN subproject. We continue following the original "Rolling with Ruby on Rails" tutorial at Part 2, constructing a non-scaffold app with two entities.
Our next task is to add a "Category" entity to our data model. Each Recipe will belong to one Category (from Recipe, a many-to-one) relationship. Our data model is defined in cookbook.hbm.xml, so let's get editing - add the new Category entity as follows:
<class name="Category" table="category"> <id name="id" type="int"> <generator class="native"/> </id> <property name="name" type="string" length="50"/> </class>
and then add the relationship column in Recipe:
<many-to-one name="category" class="uk.org.ponder.rsf.cookbook.Category" column="recipe_id"/>
Finally, update our autogenerated java model by issuing maven generate-hibernate-javamodel from the project root - Category.java will now appear in the generated-java directory.
We will use the scaffold version of the producers for Category, but the Recipe views will need to be updated in various ways. The most interesting changed view is recipe-edit, let's concentrate on that one. Firstly, here's the obligatory screenshot:
(No effort has been made to restyle this - since the template is all HTML, we can easily devolve this task to people with HTML skills :P)
The date entry component in this view has disappeared - this has been replaced by the requirement that each recipe be given the current date when it is edited. Secondly, there has been a new component added, the drop-down box from which we choose the recipe's category.
Other than these two changes, all the components in this view are standard input, link or command components that we have already met.
Let's look at the dropdown first - this is a selection control like the month selection control from the "basic" app, but it is somewhat more complex in that it needs to be propertly tied into the entity model.
Firstly, here is the definition of the component for the dropdown:
<component type="select" id="recipe-category"> <optionlist> <valuebinding>#{categories-all}</valuebinding> <!-- Will use the "id" field of category to represent in request --> <resolver type="elref">#{fieldGetter.id}</resolver> </optionlist> <optionnames> <!-- Will use the "name" field of category to render in the list --> <resolver type="elref">#{fieldGetter.name}</resolver> </optionnames> <selection type="input"> <!-- The selected value in the control is the id of the recipe's category --> <valuebinding>#{*.category.id}</valuebinding> <!-- The special decode-time bean required to remap category id back onto Object --> <darreshaper>#{id-defunnel}</darreshaper> </selection> </component>
The basic structure is familiar from the "month" selector - there are three UIBound controls named optionlist, optionnames and selection, but some of them have unfamiliar fields set. Firstly, the optionlist has a resolver field.
ID resolver and name resolver#
The value binding, #{categories-all} refers to a HQL query bean that returns all category objects - this is the set of choices which are available to the user from this control. However, since this is an entity bean, we don't expect to transfer the entire bean into the rendered view, and have it returned - equality of these beans is by ID, so we would like to encode the identity of each category object in the UI by using its id field. This is done by the resolver field - we met the resolver before in the recipes view in the basic app - there we used it to render a java.util.Date object into a suitable text form. Here we will use a standard RSF bean, the fieldGetter. fieldGetter is an application-scope bean that can be used to compute any field of another bean - in this case fieldGetter.id is a bean (of type BeanResolver that will compute the id field of any bean. So here entered as the resolver field in the optionlist, will make sure that this list is coverted from a list of Category objects into a list of Category IDs before it enters the UI.
The optionnames component uses the same trick, only refers to the bean fieldGetter.name to this time render the category objects into their names - in this form they will form the text of the HTML <option> control.
ID defunneller#
The final wrinkle here is caused by the fact that we expect the user's selection to be applied directly to the category field of the Recipe - the problem is, that the selection control is set up to let the user be selecting category IDs. If we didn't do something special here, the selection result, when submitted, would cause an assignment to #{Recipe.2.category.id, say, which would probably simply corrupt our Hibernate model. What we actually want is to assign to the category itself, since this is what Hibernate expects - if the user selects the category with id 3, for example, we want the submission to cause the assigment of bean #{Category.3} to the path #{Recipe.2.category} .
This is such a common requirement that RSF defines a special bean, #{id-defunnel} to be used in just this situation - it is an application scope bean of type DARReshaper, which transforms one DataAlterationRequest into another. You could see the effect of the darreshaper as inverse to the effect of the resolver - the resolver maps Objects into IDs before they are rendered in the UI, and the reshaper maps them back into Objects when the request is received. You probably don't want to be bothered with the details of this - the basic result is, for entity selection dropdowns of this sort, you must use the following reshaper on the actual selection control:
<darreshaper>#{id-defunnel}</darreshaper>
More exploders#
We can take care of the other requirement of this view, that it updates the date field of the recipe to the current date, also without use of any Java code. We met BeanExploders in the basic app, where we exploded the date transit bean in order to make "validator" instances for the request. Here we will simply explode java.util.Date instances - using the behaviour that the exploder, if just given a class name, will default-construct that class in the request scope, we can declare
<bean id="newdate" parent="beanExploder"> <property name="beanClass" value="java.util.Date"/> </bean>
in requestContext.xml, and then all paths of the form #{newdate.xxxx} will refer to freshly constructed Date objects in the request scope, holding the current time.
So, applying the EL binding
<parameter type="elbinding"> <valuebinding>#{*.date}</valuebinding> <rvalue type="elref">#{newdate.1}</rvalue> </parameter>
in the components definition for recipe-edit.xml means we are done with this view.
Finishing off the app#
To help user navigation, we define an index view for the app that provides easy access to the listing pages, and make it the default view:
<view> <component type="internallink" id="new-recipe"> <viewparams>/recipe-edit/new 1?mode=new</viewparams> </component> <component type="internallink" id="recipes-all"> <viewparams>/recipes</viewparams> </component> <component type="internallink" id="categories-all"> <viewparams>/categories</viewparams> </component> <defaultview>true</defaultview> </view>
Finally, we would like to help navigation round the app in general - the Ruby example does this by means of a "layout" which appends standard links as a footer around each page. At the time of writing (March 2006) RSF support for multi-file view templates is not finished, so we will for the moment do this manually - in fact we need only add a link to the index to the end some of the views.
<component type="internallink" id="index"> <viewparams>/index</viewparams> </component>
While multi-file templates will be a very general mechanism, it's worth pointing out that keeping your view templates in a single file makes it much easier to preserve full HTML previewability.
Using Javascript#
One interesting feature of the ROR app is the "confirmation dialog" that appears on trying to delete a recipe. How will we achieve this in RSF?
The clue to our approach is found in the "previewability" mantra that we keep recurring to - if this box requires some Javascript, we'd better write some Javascript! In this way the Javascript can be tested and reviewed along with the rest of the static HTML in the app. The ROR approach might appear slick, but are you comfortable with some framework automatically generating some Javascript in the middle of your page? I'm certainly not.
Putting some Javascript in the HTML is quite straightforward - the "tricky bit" is getting it to coordinate with information rendered by RSF - as you can see from the screenshot in the ROR tutorial (figure 5), the confirmation dialog text includes the name of the recipe to be deleted. The "correct" approach to this bears in mind that RSF renders the page on an XML basis - so we will let RSF render the relevant recipe title into a hidden <span> tag, and let the Javascript fish it out from there. To keep previewability, we will write some placeholder text into the <span> that we will take care to provide the correct final full ID that if will be rendered with, in addition to its RSF ID. Recall that RSF will synthesize HTML ids for every control that are not only globally unique, but in addition (and in contrast to JSF, for example), fully predictable, making it an extremely Javascript-friendly environment.
So, we make the following modifications to the template:
<style type="text/css"> .hidden { display: none; } </style> <script type="text/javascript" src="../javascript/getvalue.js" rsf:id="scr=rewrite-url"> </script> ... <span class="hidden" rsf:id="confirm-title" id="confirm-title:1">Hot Chips</span> ... <input type="submit" rsf:id="recipe-destroy" id="recipe-destroy:1" value="(delete)" onclick='return confirm("Really delete " + getRelativeValue(this.id, "confirm-title") + "?")'/>
The "getvalue.js" script is a standard script that abstracts over the various browser-dependent technologies for locating DOM/HTML elements (any suggestions for improvements to this are welcome, I am not a Javascript expert). getRelativeValue accounts for a fact that this component is in a repetitive domain and gets hold of the hidden "confirm-title" span in this table row rather than any other one. Note that this is only possible because RSF "full IDs" are composed in a regular way from a colon-separated list of path components.
Before even trying to build this app, experiment with the HTML templates in the filesystem and check that this dialog works (as well as relative links &c) - clearly this is a marvellously more productive way to debug tricky Javascript/AJAX components, decoupled from expensive build/test cycles.
Make sure you place "correct" (or at least consistent) HTML {id}s on these components so that the Javascript will perform the same computations in the static template as it will in the dynamic view - the IKAT renderer will overwrite the id attribute at run-time when it has the actual recipe list in its hands, so the local IDs you supply (in this case "1") are arbitrary.
Then all we need to do is add the corresponding output component to recipes.xml
<component type="output" id="confirm-title"> <valuebinding>#{*.title}</valuebinding> </component>
We're done#
Everything else about this app is fairly straightforward given our coverage of basic RSF idioms in the "scaffold" version of this app and also the NumberGuessing app. So - there we are! A complete Hibernate-enabled, HTML-templated webapp created, not with 47 lines of Ruby code but in fact no lines of any kind of code whatever! Of course the XML components we have had to write, in compensation, are somewhat verbose - you would in practice use this strategy if i) you were auto-generating applications via some kind of XML transform, or ii) had a user/developer community who was not too familiar with Java and preferred this kind of flexible "prototyping" approach.
For more long-lived projects, where specialised Java developers were involved, you would probably have written the view producers for this app in Java - there is a straightforward one-for-one translation between XML producers and Java producers, so this can easily be done at any time in a project. So, let's round off our treatment of this app by translating it into Java.
Head - Hibernate Cookbook
Page 1 - Query beans, templates and views
Page 2 - Switches, Replicators and transit beans
Page 3 - The main app - entity selectors
Page 4 - Let's hear that one more time - this time in Java
Add new attachment
List of attachments
Kind | Attachment Name | Size | Version | Date Modified | Author | Change note |
---|---|---|---|---|---|---|
png |
cookbook-category-all-recipes.... | 18.4 kB | 1 | 19-Jul-2006 09:36 | 192.168.101.139 | |
png |
cookbook-category-edit-recipe.... | 19.5 kB | 1 | 19-Jul-2006 09:36 | 192.168.101.139 |