TurboManage

David Chandler's Journal of Java Web and Mobile Development

  • David M. Chandler


    Web app developer since 1994 and former Developer Advocate with Google now residing in Colorado. Besides tech, I enjoy landscape photography and share my work at ColoradoPhoto.gallery.

  • Subscribe

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 241 other followers

  • Sleepless Nights…

    April 2010
    S M T W T F S
    « Mar   May »
     123
    45678910
    11121314151617
    18192021222324
    252627282930  
  • Blog Stats

    • 857,971 hits

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:

  1. Hard-code your presenter to ListBox instead of an interface (very unsatisfying)
  2. Register ValueChangeHandlers in the view and let the view call a method on the presenter (bi-directional MVP)
  3. Punt and make a fake model item representing your prompt
  4. 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!

Advertisements

15 Responses to “SelectOneListBox for use with GWT+MVP”

  1. Eric Jablow said

    GWT tends to make well-known HTML techniques hard. For example, I just figured out how to use UiBinder and Document.get().createUniqueId() to include <label>s on my pages. I’ve seen someone subclass the Grid and FlexTable 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:

    HasValues<PrayerList> && HasValue<PrayerList> getPrayerLists();  // HasValues has the set methods.
    

    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.

  2. Phil Ives said

    a little bit simpler, ListBox with HasValue

  3. 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.

    • 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.

  4. 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!

  5. 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.

  6. 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

  7. 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

  8. 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

  9. Sydney Henrard said

    In the constructor, calling setSelections before setFormatter will throw a NullPointerException on the formatter on line 65

  10. cong ty thiet ke noi that…

    […]SelectOneListBox for use with GWT+MVP « TurboManage[…]…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: