SelectOneListBox for use with GWT+MVP
Posted by David Chandler on April 1, 2010
According to the Model-View-Presenter approach to GWT development, presenters should not know about specific Widgets in views, but rather call methods on interfaces like HasValue and HasClickHandlers. In practice, this works well with relatively simple widgets like TextBox whose behavior can be described to the presenter in terms of a single interface such as HasValue. However, GWT doesn’t yet provide suitable interfaces for all Widgets. One such example is the ListBox, which implements only HasChangeHandlers and HasName. Wouldn’t it be nice if there were a HasValue equivalent for ListBox that would let you get and set the selected value as well as populate the list?
Here’s my idea for such an interface:
package com.turbomanage.gwt.client.ui.widget; import java.util.Collection; import com.google.gwt.user.client.ui.HasValue; /** * MVP-friendly interface for use with any widget that can be populated * with a Collection of items, one of which may be selected * * @author David Chandler * * @param <T> */ public interface HasSelectedValue<T> extends HasValue<T> { void setSelections(Collection<T> selections); void setSelectedValue(T selected); T getSelectedValue(); }
In retrospect, the method name setSelections may be a bit confusing. It refers to all available options (perhaps setOptions instead?), not the one selected value.
And here’s a SelectOneListBox widget that implements the interface (astute readers may note a nod to JSF in the name…). It extends GWT’s ListBox, which simplifies implementation of the methods related to HasValue.
package com.turbomanage.gwt.client.ui.widget; import java.util.ArrayList; import java.util.Collection; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.ListBox; /** * A ListBox that can be populated with any Collection * * Published under Apache License v2 * * @author David Chandler * @param <T> */ public class SelectOneListBox<T> extends ListBox implements HasSelectedValue<T> { public interface OptionFormatter<T> { abstract String getLabel(T option); abstract String getValue(T option); } private boolean valueChangeHandlerInitialized; private T[] options; private OptionFormatter<T> formatter; public SelectOneListBox(Collection<T> selections, OptionFormatter<T> formatter) { setSelections(selections); setFormatter(formatter); } public SelectOneListBox(OptionFormatter<T> formatter) { this(new ArrayList<T>(), formatter); } public void setFormatter(OptionFormatter<T> formatter) { this.formatter = formatter; } @SuppressWarnings("unchecked") @Override public void setSelections(Collection<T> selections) { // Remove prior options if (options != null) { int numItems = this.getItemCount(); int firstOption = numItems - options.length; for (int i=firstOption; i<numItems; i++) this.removeItem(firstOption); } options = (T[]) selections.toArray(); for (T option : options) { String optionLabel = formatter.getLabel(option); String optionValue = formatter.getValue(option); this.addItem(optionLabel, optionValue); } } @Override public T getSelectedValue() { if (getSelectedIndex() >= 0) { String name = getValue(getSelectedIndex()); for (T option : options) { if (formatter.getValue(option).equals(name)) return option; } } return null; } @Override public void setSelectedValue(T value) { if (value == null) return; for (int i=0; i < this.getItemCount(); i++) { String thisLabel = this.getItemText(i); if (formatter.getLabel(value).equals(thisLabel)) { this.setSelectedIndex(i); return; } } throw new IllegalArgumentException("No index found for value " + value.toString()); } /* * Methods to implement HasValue */ @Override public T getValue() { return this.getSelectedValue(); } @Override public void setValue(T value) { this.setValue(value, false); } @Override public void setValue(T value, boolean fireEvents) { T oldValue = getValue(); this.setSelectedValue(value); if (fireEvents) { ValueChangeEvent.fireIfNotEqual(this, oldValue, value); } } @Override public HandlerRegistration addValueChangeHandler(ValueChangeHandler<T> handler) { // Initialization code if (!valueChangeHandlerInitialized) { valueChangeHandlerInitialized = true; super.addChangeHandler(new ChangeHandler() { public void onChange(ChangeEvent event) { ValueChangeEvent.fire(SelectOneListBox.this, getValue()); } }); } return addHandler(handler, ValueChangeEvent.getType()); } }
The SelectOneListBox constructor takes any collection and an OptionsFormatter which tells it how to get the label and value associated with each item in the collection. Here’s an example of its use:
selectPrayerList = new SelectOneListBox<PrayerList>(new OptionFormatter<PrayerList>() { @Override public String getLabel(PrayerList option) { return option.getName(); } @Override public String getValue(PrayerList option) { return option.getId().toString(); } });
And finally, an example of populating the SelectOneListBox in a presenter:
HasSelectedValue<PrayerList> getPrayerListFilter(); ... display.getPrayerListFilter().setSelections(result.getPrayerLists());
Because HasSelectedValue extends GWT’s HasValue, you can also add ValueChangeHandlers in the presenter like this:
display.getPrayerListsBox().addValueChangeHandler(new ValueChangeHandler<PrayerList>() { @Override public void onValueChange(ValueChangeEvent<PrayerList> event) { ... event.getValue() ... } });
Now, the problem always comes up (and this is probably the reason that GWT’s ListBox doesn’t implement a HasValue-like interface already), how do you add to the list things which are not model items, that is, not in the Collection? A common example is a “Select one” or “New…” prompt to appear in the list box. This is not a problem if you’re using ListBox directly, as you can add any String with addItem(). But it is a problem for SelectOneListBox. Because it implements HasSelectedValue, all options must be of type T. So you can do one of four things:
- Hard-code your presenter to ListBox instead of an interface (very unsatisfying)
- Register ValueChangeHandlers in the view and let the view call a method on the presenter (bi-directional MVP)
- Punt and make a fake model item representing your prompt
- Don’t put such items in list boxes. It confuses screen readers, anyway.
I think #4 is actually the most correct answer and fight for it wherever I can. But when screen real estate is precious, I’m willing to break the rules a little. Since I’m using gwt-presenter, #2 is not an option, which leaves #3. It offends my sensibilities, but it’s quite easy and it works. Here’s an example:
ArrayList<PrayerList> prayerLists = new ArrayList<PrayerList>(); // -1 represents an ID that will never be used in a real PrayerList prayerLists.add(new PrayerList(-1, "Select one")); prayerLists.addAll(result.getLists()); display.getPrayerListFilter().setSelections(prayerLists);
While we’re at it, we can even add a little bit of CSS to style our fake -1 object with a line underneath to act as a menu separator:
/* Select one */ option[value="-1"] { border-bottom: 1px solid black; padding-bottom: 2px; }
To me, this little hackery is worth the convenience and correctness of HasSelectedValue.
HasSelectedValues (plural) and SelectManyListBox are left as an exercise to the reader (for now, anyway).
Enjoy!
Eric Jablow said
GWT tends to make well-known HTML techniques hard. For example, I just figured out how to use
UiBinder
andDocument.get().createUniqueId()
to include <label>s on my pages. I’ve seen someone subclass theGrid
andFlexTable
widgets to add a <caption> element. I have seen no one successfully create an <optgroup> element, and that is what you really need for the “Select One” option.I’m beginning to think that the technique Swing uses—ListModels and TableModels and renderers—isn’t suited for GWT and MVP. The presenter should not give the view model objects and a renderer, but should give the view primitives and let the view render them as it is programmed to.
if only Java let one declare a variable as implementing more than one interface, or had duck-typing. Then, your
SelectOneListBox
could be vended as:I’m not sure I want the interface that sets values also being the interface that gets values. I can imagine setting one type of value but getting something different. But if the model objects stay in the presenter, that goes away.
What other interfaces does GWT need? I’d like to see a HasCommand interface for menu items, and a simplified button that takes a Command too.
David Chandler said
Great thoughts, Eric. I like the HasCommand idea.
Phil Ives said
a little bit simpler, ListBox with HasValue
https://gist.github.com/337159/3ec29ec0e41e7f554e876429262c0e2c8d0166b5
Stephen said
For your dummy value, my guess is that it could be something that your list box widget/view implementation hides completely from the presenter. E.g. something like:
HasSelectionBox {
void setFirstRow(String text);
void setValues(List values)
}
Then when calling the real ListItem.addItem, before doing for (T value : values), you see if firstRowText != null, do a quick addItem(“DUMMY”, firstRowText), then iterate your values.
You’d then also have to add some logic in your change listener that said if ListItem.getValue() == “DUMMY”, you suppress the change event and not fire a T change event. And also if they call HasSelectionBox.getValue and the current value is DUMMY, you return null instead.
Haven’t actually tried it though.
However, perhaps this is too tricky, as I also agree with Eric’s sentiments that the closer views are to the low-level widgets, the better. When I started, I was making my views too smart. But the dumber they are, the more logic is in the presenter, and the easier everything is to test.
David Chandler said
Thanks, Stephen. Perhaps I spent too much time in JSF land. I had exactly the same problem there with listboxes, but I’d like to think I could make a data-bound widget work vs. using only Strings in views. There’s some stuff coming in GWT “bikeshed” like SelectionModel that will hopefully solve some of these problems.
Stephen said
Cool–if you want to go data-bound, then I think only binding real objects and letting the view impl insert/suppress the dummy first one makes sense.
I stumbled across the bikeshed stuff as well somehow, and have been following their commits. So far I haven’t been able to glean much from it. There seems to be an awful lot of churn, so it is hard for me to follow. It will be interesting to see what results when things settle down.
I’m thinking whatever they’re up to would be a good demo/session for Google IO, so hopefully they’ll get it done by then.
Dan Billings said
May I suggest adding an OptionFormatter argument to the “setSelections”?
I initially tried to use it without providing a formatter, which failed.
I then called setSelections before setFormatter, which also failed, because the formatter needs to be set before “setSelections” knows how to process the items.
I ended up adding this to my own implementation. I just thought I’d share my experience and suggestion.
Thanks for sharing this!
David Chandler said
Good catch, Dan. Since an OptionFormatter is required, setSelections should at least explicitly handle that error condition.
Thanks! /dmc
Dan Billings said
Hey David,
Check out the 2010 I/O GWT testing video. He makes a case for a “dumb view contract” in which you would want your checkbox to be as stupid as it comes with GWT (no getSelected() or really any other state getters).
Anyway, the point is that burden then goes to the presenter to maintain the selected object, not the combobox itself.
Saket Bansal said
i am new to GWT, can you give example of View class using this ListBox, i am using @UiTemplate, and for this i have to declare control as ListBox , i am trying to cast it to SelectOneListBox but it is giving exception…
– Saket
Saket Bansal said
This issue was resolved by using custom List Box in ui.xml file at the place of standard ListBox
Following link helped
http://code.google.com/intl/sv-SE/webtoolkit/doc/latest/DevGuideUiBinder.html#Using_a_widget
David Chandler said
Super, thanks for posting back.
Ron Houseman said
David,
I’m new to GWT. Can you provide an example of the View class using the SelectOneListBox? Expanding on your example from above would be great.
Thanks,
Ron
Sydney Henrard said
In the constructor, calling setSelections before setFormatter will throw a NullPointerException on the formatter on line 65
cong ty thiet ke noi that said
cong ty thiet ke noi that…
[…]SelectOneListBox for use with GWT+MVP « TurboManage[…]…