WOPopUpButtons in a WORepetition
Thursday, October 19th, 2006Abstract
In a WOForm in a WOComponent, you can repeat a dynamic element like WOPopUpButton by wrapping it in a WORepetition, and still extract the selected objects during binding synchronization by using a custom set accessor.
Getting selected objects from WOPopUpButtons in a WORepetition
by Apoorva Muralidhara
Here’s a useful trick that allows you to repeat form elements on a page with a WORepetition and extract the selected objects with very little code. This simple idea is the missing link that allows you to use WORepetition to get rid of more duplication in WOComponents.
PizzaRobot: An Example
A visionary businessman plans to install robotic pizza kiosks in New York, Los Angeles, and Atlanta in early 2007. These fully automated kitchens will make and toss dough, add fresh mozzarella and toppings, and bake a fresh pizza to the customer’s specifications. After obtaining a first round of funding, PizzaRobot hires you to develop a pizza ordering application in WebObjects.
The First Story
The first customer story card says:
“The user can specify up to eight toppings for a pizza pie. There will be eight topping dropdowns. Topping order doesn’t matter, and duplicate toppings will be treated as if chosen once.
“The initial list of toppings will be as follows: anchovy, artichoke, arugula, broccoli romanesco, chorizo, escargot, ginger, habanero, ham, kebab, kielbasa, leek, mayonnaise, onion, paneer, pepperoni, prosciutto, rabbit, ramp, tofu”
EOModel and Database Preliminaries
You create three entities and corresponding tables—Pizza, Topping, and PizzasToppings—with a flattened many-to-many relationship “toppings” from Pizza to Topping, using PizzasToppings as the correlation table. EOGenerator gives you the Pizza methods
public NSArray toppings()
public void setToppings(NSArray value)
public void addToToppings(Topping topping)
public void removeFromToppings(Topping topping)
You populate the Topping table with the given values, using a SQL script containing insert statements that you check into your Subversion repository.
The PizzaPage Component
You create a PizzaPage.wo WOComponent, add a WOForm, and create a pizza:
protected Pizza pizza;
public void awake() {
super.awake();
pizza = new Pizza();
}
The topping popups will require an array of all toppings, so you add
public NSArray allToppings() {
return EOUtilities.objectsForEntityNamed(session().defaultEditingContext(), "Topping");
}
and an instance variable for iterating over the toppings:
public Topping topping;
Now you need to put eight WOPopUpButtons onto PizzaPage.
The Eightfold Way: The Cut-and-Paste Solution
You could, of course, do the following:
Add eight public instance variables to PizzaPage.java:
public Topping selectedTopping1;
public Topping selectedTopping2;
public Topping selectedTopping3;
public Topping selectedTopping4;
public Topping selectedTopping5;
public Topping selectedTopping6;
public Topping selectedTopping7;
public Topping selectedTopping8;
Add eight WOPopUp buttons to PizzaPage.html:
Add eight sets of bindings to PizzaPage.wod:
ToppingPopUpButton1: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping1;
}
ToppingPopUpButton2: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping2;
}
ToppingPopUpButton3: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping3;
}
ToppingPopUpButton4: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping4;
}
ToppingPopUpButton5: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping5;
}
ToppingPopUpButton6: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping6;
}
ToppingPopUpButton7: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping7;
}
ToppingPopUpButton8: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping8;
}
You want to ignore topping order and duplication, so you use an NSSet. Your save() method looks like this:
public ThankYouPage save()
{
NSSet selectedToppings =
new NSSet(new Topping[] {selectedTopping1, selectedTopping2,
selectedTopping3, selectedTopping4,
selectedTopping5, selectedTopping6,
selectedTopping7, selectedTopping8});
pizza.setToppings(selectedToppings.allObjects());
session().defaultEditingContext().insertObject(pizza);
session().defaultEditingContext().saveChanges();
return (ThankYouPage)pageWithName("ThankYouPage");
}
Your Clone Army Smells of Duplication
This repetitive solution has Duplicated Code Smell, which Martin Fowler talks about in his book Refactoring. Your cut-and-pasted code clone army can get out of sync, and other developers will have to do a side-by-side comparison to perceive that there are eight nearly identical copies of everything. If RobotPizza asks for the noSelectionString to be “Choose a scrumptious topping” instead of ” — “, you’ll have to do change the string in eight places. And if they upgrade their auto-pie hardware to allow eleven toppings, you’ll have to cut-and-paste (or kill-and-yank, for us Emacs weenies) three more public instance variables (in PizzaPage.java), three more tags (in PizzaPage.html), and three more “ToppingPopUpButton” binding-set entries (in PizzaPage.wod), and not forget to add the three additional toppings to the Topping[] array in PizzaPage.save().
Express Yourself With a WORepetition
Like Madonna or N.W.A., you must “express yourself.” You, the human (or blog-reading extraterrestrial geek) writing the code, know that there are “eight topping popup buttons,” but you’re only telling WebObjects about them one at a time, without conveying their essential identity. To use more of WO’s expressive power, create a WORepetition with “count” bound to 8:
ToppingPopUpRepetition: WORepetition {
count = 8;
}
You wrap this repetition around a single WOPopUpButton:
ToppingPopUpButton: WOPopUpButton {
displayString = topping.name;
item = topping;
list = allToppings;
noSelectionString = " --- ";
selection = selectedTopping;
}
with
public Topping topping, selectedTopping;
in PizzaPage.java.
All seems to be in order. So what’s the problem?
The Solution: Render Unto Me the Pizza Toppings I Have Chosen
How do you get the selected toppings from the eight generated WOPopUpButtons? Ay, there’s the rub. Above I’ve bound “selection” to “selectedTopping,” but with only a single selectedTopping instance variable, the up to eight toppings chosen by the user will fall through your fingers like so many pebbles.
[I pause for a moment to increase the suspense.]
The simple, elegant solution illustrates a pattern for getting selections from dynamic elements in a WORepetition.
Keep the public instance variable selectedTopping—you need it so that WebObjects can resolve valueForKey(”selectedTopping”) on the PizzaPage component when first rendering the popups in appendToResponse().
Add an instance variable for the set of selected toppings. You’ll be adding to it, so it has to be an NSMutableSet:
public NSMutableSet selectedToppings;
Add a line to your awake() method:
selectedToppings = new NSMutableSet();
Here’s the trick: Add a set accessor for selectedTopping that, instead of setting the selectedTopping instance variable, adds the selected topping to selectedToppings:
public void setSelectedTopping(Topping aSelectedTopping) {
selectedToppings.addObject(aSelectedTopping);
}
Now your save method looks like this:
public ThankYouPage save()
{
pizza.setToppings(selectedToppings());
session().defaultEditingContext().insertObject(pizza);
session().defaultEditingContext().saveChanges();
return (ThankYouPage)pageWithName("ThankYouPage");
}
where
public NSSet selectedToppings() {
return selectedToppings.immutableClone();
}
Simpler and cleaner. When the client wants you to modify the noSelectionString, you’ll only have to change it in one place. And when they finally manage to upgrade their pizzabots to handle eleven toppings, you’ll just have to change the “8″ to an “11.”
