TurboManage

David Chandler's Journal of Java Web and Mobile Development

  • David M. Chandler


    Web app developer since 1994 and Google Cloud Platform Instructor 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 223 other followers

  • Sleepless Nights…

    April 2010
    S M T W T F S
     123
    45678910
    11121314151617
    18192021222324
    252627282930  
  • Blog Stats

    • 1,029,242 hits

Archive for April 1st, 2010

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!

Posted in Google Web Toolkit, Model-View-Presenter | 15 Comments »

 
%d bloggers like this: