Hello World, I Need Some Groceries
- this example shows how we can use the BTemplate to generate a list of data.
Taking a look at the Source
Ok, let's start by looking at the
source. There really aren't too many differences between HelloWorld 2 and previous
examples.
One of the first things we see is that in this example we are registering a second
model with the template component. The code looks something like this:
String[] items = new String[]
{"Cabbage", "Carrots", "Pickles",
"Sugar", "Wheaties", "Flour", "Potatoes"};
String[] qtys = new String[] {"3 heads", "1 bag", "1 jar",
"5 lbs", "3 bxs", "50 lbs", "25 lbs"};
templateComp.addModel(new GroceriesModel(items, qtys));
Here, we're basically passing in two arrays of data (items, qtys) which will serve as
the underlying data model for our TemplateModel implementation.
Ok, so what does the TemplateModel look like? Well, if we look further down in the code
we see it looks somethign like this:
class GroceriesModel extends
AbstractTemplateModel implements IterativeModel {
String[] items = null;
String[] qtys = null;
int cntr = -1;
//constructor
public GroceriesModel(String[] iitems, String[] iqtys) {
items = iitems;
qtys = iqtys;
}
//register the model by name
public String getName() {return "Groceries";}
//provide items by key
public Object getItem(String key) {
if (key.equals("Item")) return
items[cntr];
else if (key.equals("Qty")) return
qtys[cntr];
else return
super.getItem(key);
}
//prepare for iteration
public void preIterate() {
cntr = -1;
}
//return true if there are more rows in the data model
public boolean hasNext() {
return (cntr<(items.length-1));
}
//load the next row in the model
public void loadNext() {
cntr++;
}
//cleanup after iteration
public void postIterate() {}
}
While this may look like a lot more code, a quick glance reveals that it's actually
still very straightforward. The most obvious difference is that here the TemplateModel
also implements an interface called IterativeModel.
This interface defines 4 key methods that allow the component to iterate through the
model's data.
preIterate() - this method
is called when the component wishes to begin iterating over data. It gives the model a
chance to prepare itself. Here we just reset our position counter to -1.
hasNext() - this method
allows the model to tell the component whether or not it has any more data. In this case,
we can return true as long as the counter doesn't exceed the number of items in the
underlying model.
loadNext() - the component
calls this method when it actually wants the model to iterate forward to the next record
in the data set. In this case, all we do is increment our counter; in a more realistic
example we might load the next record from an iterator or result set.
postIterate() - this method
is called when the iteration is complete (after the model returns false for the hasNext()
method). It gives the model a chance to perform any cleanup. Here we don't need to do
anything special.
By implementing these methods, the component can iterate through the data in the
GroceriesModel. Every time it requests a specific key (here "Item" or
"Qty") the model just returns the value from the current row of data.
Well now, that wasn't too bad, was it! So now you probably wonder what kind of
directives we had to use in the template to make this happen. Good question...read on for
the answer!
Taking a look at the Template
The template code is now a little more complicated than in previous examples, but it is
still very simple when compared to other templating approaches (and that's the good
news...this is as complicated as it gets! So once you understand this part you will have
mastered all you need to know to effectively use the the BTemplate). Let's start by taking
a look at
the actual template here. Most of it is identical to what we've seen in previous
examples. The part that we are interested in is this:
<p>Here's our simple shopping
list:</p>
<ul>
<li class="Dir::Iterate_Start.Groceries
Dir::Iterate_Next.Groceries">
<span
class="Dir::Get_Data.Groceries.Qty">3 cans</span>
<span
class="Dir::Get_Data.Groceries.Item">Corn</span>
</li>
<li class="Dir::Iterate_End.Groceries Dir::Discard">4
bags Peas</li>
<li class="Dir::Discard">1 bunch Bannanas</li>
<li class="Dir::Discard">...</li>
</ul>
Here we see a couple of interesting things. First of all, notice that we are dealing
with an HTML list structure. The designer has populated the template with a couple of rows
to demonstrate how the grocery list would appear. We want to actually iterate through the
model and replace the mockup data with the real data from our GroceriesModel.
The way we do this is by using an "iterate" directive. There are actually 3
separate directives, each of which is used to perform a different part of the iteration
process.
Dir::Iterate_Start.Groceries
- this directive tells the component to find the Groceries model and prepare it for
iteration. The model must implement IterativeModel for the component to be able to
implement over it.
Dir::Iterate_Next.Groceries
- this directive tells the component to notify the model that it needs to locate the next
row of data.
Dir::Iterate_End.Groceries
- this directive tells the component where the iteration block ends. When the iterator
hits the Iterate_End block, it will loop back up to the Iterate_Start block and continue
through the template until the model reports that it is out of data.
Once the model is out of data, the template component jumps to the
Iterate_End block and continues processing the template from that point on.
Now, during the iteration, every node from Iterate_Start to Iterate_End
gets repeatedly processed. As we work through the template, we encounter the familiar
Get_Data directives which load Item and Qty values from the model (for whatever the
current row is). The resulting HTML looks something like this:
<LI>
<SPAN>3 heads</SPAN>
<SPAN>Cabbage</SPAN>
</LI>
<LI>
<SPAN>1 bag</SPAN>
<SPAN>Carrots</SPAN>
</LI>
...
Cool! That seems to be doing exactly what we wanted.
Now, let's take a look at the directive that immediately follows
Iterate_End. It's very simple: Dir::Iterate_Discard.
This directive does just what it sounds like: it tells the template component that it may
simply discard this particular node because it's not actually part of the template. Note
that in the specific case here this directive never gets processed until after the
iteration loop is complete. This is because as soon as the component hits the Iterate_End
directive, it immediately loops back to the start block.
That about covers it! Pretty simple, huh?
Understanding Directives
So, let's take a quick look at the complete list of directives:
- Dir::Get_Data.<model>.<key> - get data from the specified
model based on key and place it into the document
- Dir::Set_Attr.<model>.<key>.<attr> - similar to the
previous command, except that this directive locates the data for the specified key,
converts it to a String and places it in the named attribute setting. Useful when you need
to set an attribute (like the alt attribute in an image tag).
- Dir::Iterate_Start.<model> - start an iteration on the specified
model
- Dir::Iterate_Next.<model> - ask the model to load the next record
(if it exists)
- Dir::Iterate_End.<model> - marks the end of an iteration block
- Dir::Discard - discards the current element so that it is not included
in the final DOM output
Yep, that's it! Pretty straightforward.
Now, one of the questions that comes up a lot is "So what other directives are you
going to add next?" The answer is "We don't think we will need to add any
more". Here's why.
First of all, you'll notice we don't have any conditional directives. This is
intentional; it's very easy for a directives based approach to blossom into a full blown
scripting language. We want to avoid this because we think it's a bad idea to merge
content with code. Now, some people may argue that that's what we've already done, but we
think there's a difference. We are embedding "tags" in the markup, but really we
are just following a naming convention that allows the component to interpret what the
designer had in mind in terms of layout. In other words, we could simply use an id naming
convention (which would be illegible to us but meaningful to the component); we've chosen
the more legible route because it makes it easier for designers to add these "layout
markers" to the markup if they desire. Of course, if you don't like anything in you
*ML, you don't have to go this route.
At any rate, the point here is that with the current implementation of directives, we
really are not adding "code" to the markup language. So then, what about
conditional type operations? What if the designer wants a certain row to be used in one
case and a different row to be used in another? To accomodate this scenario, we've allowed
for custom directives. A custom directive follows the same directive format we're already
familiar with: (Dir::<command>.<model>.<key>.<attr>).
All commands are passed into the TemplateModels processDirective() method, which allows
the model developer to tell the designer "look, here's a custom directive you can use
for this model".
As an example, the developer could create a Veto_If_Null directive. When the designer
specifies this directive, the model could check to make sure that the value for the key
specified is not null. If it is, the model returns false (meaning, "No, don't process
this directive", which causes the model to discard the current node and continue
processing. This type of behaviour can be used quite well to perform simple conditional
tasks, like dropping out blank rows in a report. We have intentionally not added this type
of things as a standard directive in order to keep the number of directives very small.
If for some reason the custom directive approach is too limiting, we'd recommend using
an alternate component rather than trying to modify the directives. Remember, simple,
simple, simple. Simple is a good thing! :-) |