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…

    December 2009
    S M T W T F S
    « Nov   Jan »
     12345
    6789101112
    13141516171819
    20212223242526
    2728293031  
  • Blog Stats

    • 865,597 hits

An MVP-compatible EnumListBox for GWT

Posted by David Chandler on December 7, 2009

A frequent request on the GWT and gwt-presenter forums is for a ListBox that implements HasValue like a TextBox. I recently needed one myself, and thought it would be especially cool if I could use it with a Java enum type like this:

	public static enum Frequency {DAILY, WEEKLY, MONTHLY};

	private ConstantsWithLookup enumLabels = GWT.create(EnumLabels.class);
	private EnumListBox<Frequency> freqBox;

	freqBox = new EnumListBox<Frequency>(Frequency.class, enumLabels);

In keeping with MVP philosophy, the presenter’s display interface only needs the HasValue type to get and set the selected value as well as add a ValueChangeHandler to respond to a new selection. Here as some relevant excerpts from a presenter that uses the EnumListBox:

	public interface Display extends WidgetDisplay
	{
		HasValue<Frequency> getFrequency();
	}
	...
	protected void onFirstRequest()
	{
		...
		display.getFrequency().addValueChangeHandler(new ValueChangeHandler<Frequency>()
		{
			@Override
			public void onValueChange(ValueChangeEvent<Frequency> event)
			{
				// Do something with the newly selected event.getValue()
				...
			}
		});
	}

Here’s a straightforward implementation of an EnumListBox that implements HasValue. Thanks to the gwt-ent project for the original idea for this.

package com.roa.app.client.ui.widget;

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.i18n.client.ConstantsWithLookup;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.ListBox;
import com.roa.common.client.util.EnumTranslator;

public class EnumListBox<T extends Enum<T>> extends ListBox implements HasValue<T>
{

	private final Class<T> clazzOfEnum;
	private boolean valueChangeHandlerInitialized;

	public EnumListBox(final Class<T> clazzOfEnum, final ConstantsWithLookup constants)
	{
		if (clazzOfEnum == null)
			throw new IllegalArgumentException("Enum class cannot be null");

		this.clazzOfEnum = clazzOfEnum;
		EnumTranslator enumTranslator = new EnumTranslator(constants);

		T[] values = clazzOfEnum.getEnumConstants();

		for (T value : values)
		{
//			this.addItem(constant.toString(), constant.name());
			this.addItem(enumTranslator.getText(value), value.name());
		}
	}

	public T getSelectedValue()
	{
		if (getSelectedIndex() >= 0)
		{
			String name = getValue(getSelectedIndex());

			T[] values = clazzOfEnum.getEnumConstants();
			for (T value : values)
			{
				if (value.name().equals(name))
					return value;
			}
		}

		return null;
	}

	public void setSelectedValue(T value)
	{
		T[] values = clazzOfEnum.getEnumConstants();
		for (int i = 0; i < values.length; i++)
		{
			if (values[i] == value)
			{
				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(EnumListBox.this, getValue());
				}
			});
		}
		return addHandler(handler, ValueChangeEvent.getType());
	}

}

There’s really not much to it, just a little weirdness that always comes with generics. Notice that the constructor uses an EnumTranslator to populate the labels in the ListBox. This allows you to use a standard GWT ConstantsWithLookup inteface to supply localized text for the enum values instead of the constant names. ConstantsWithLookup is just like Constants, but with the important ability to find a value dynamically without invoking a method corresponding to the property name. Unfortunately, you still have to define a method for each value of the enum in your ConstantsWithLookup class, even though it’s never used directly. Here’s a sample interface:

public interface EnumLabels extends ConstantsWithLookup {
	// Enums
	String com_mypackage_MyClass_Frequency_DAILY();
	String com_mypackage_MyClass_Frequency_WEEKLY();
	String com_mypackage_MyClass_Frequency_MONTHLY();
	String com_mypackage_MyClass_Frequency_QUARTERLY();
	String com_mypackage_MyClass_Frequency_YEARLY();

And the corresponding default properties file EnumLabels.properties:

com_mypackage_MyClass_Frequency_DAILY=daily
com_mypackage_MyClass_Frequency_WEEKLY=weekly
com_mypackage_MyClass_Frequency_MONTHLY=monthly
com_mypackage_MyClass_Frequency_QUARTERLY=quarterly
com_mypackage_MyClass_Frequency_YEARLY=yearly

And finally, here’s my EnumTranslator:

package com.roa.common.client.util;

import com.google.gwt.i18n.client.ConstantsWithLookup;

/**
 * Does a properties file lookup to get text associated with an enum value
 * Property keys use the full class name with all dots and dollars
 * converted to underscores. Keys are case-sensitive and GWT requires a
 * method in the interface that extends ConstantsWithLookup, even though
 * the method is never called.
 *
 * Example:
 * String my_package_class_Frequency_DAILY();
 *
 * In the corresponding properties file:
 * my_package_class_Frequency_DAILY=daily
 *
 * @author David Chandler
 */
public class EnumTranslator
{
	private ConstantsWithLookup constants;

	public EnumTranslator(ConstantsWithLookup constants)
	{
		this.constants = constants;
	}

	public String getText(Enum e)
	{
		String lookupKey = e.getClass().getName() + "." + e.name();
		lookupKey = lookupKey.replace(".", "_");
		lookupKey = lookupKey.replace("$", "_");
		return constants.getString(lookupKey);
	}
}

This EnumListBox is a fairly hard-wired kind of ListBox. In the near future, I anticipate refactoring along these lines:

  1. Add a constructor that takes any java.util.List, not just an Enum.
  2. Create an interface HasSelectedValue that extends HasValue by adding a populateAllSelections() method. This would allow the available selections to come from the presenter through the Display interface and is thus even better for MVP. Versions of the new method could also take a java.util.List or Enum and would replace the constructor.
  3. Ditto for a HasSelectedValues interface to deal with multiple-select type ListBoxes.

Stay tuned.

	...
Advertisements

11 Responses to “An MVP-compatible EnumListBox for GWT”

  1. Per Wiklander said

    How about doing it the Glazed Lists way? (http://www.publicobject.com/glazedlists/)

    So you have a ListBoxViewer that holds the listBox and has a getWidget() to give the widget to your panel.The viewer takes an instance of the interface ListBoxFormat in its constructor. The format implementation has a getValue(E element) that returns the value to display in the llistbox. The viewer has the required methods to set the values of the listbox and get a list of the currently selected values and so on. Probably along the way of what you’re planning to do next.

    By using viewers and formats you can do the same thing for other widgets like tables as well. So populating a table with values is as easy as tableViewer.setSource(aListOfTableRowObjects) where the objects in the list can by of any type that you can write a TableFormat for (with a getColumnValue(int column, E element) method).

    GlazedLists does this for the desktop (Swing and SWT/JFace). The cool thing there is that the viewer is aware of changes to the actual input list outside of the viewer, I don’t think that’s possible in GWT yet

    • Great thoughts, Per. The ext-gwt and gwt-incubator projects both have TableModels that implement similar ideas. Yes, I was heading toward a Formatter. I was envisioning an interface argument with a single method getLabel() that you could implement in an anonymous inner class. Could you expand a little on the value of a Viewer with getWidget() vs. just extending a widget like ListBox directly? Are you thinking to have a presenter just for the listbox? Thanks!

      • perwiklander said

        Scrap the getWidget(), but let the Viewer inherit Composite and let it choose which widget(s) to use. You could then have a getWidget() anyway to let the API user set various things on the underlying widget that you don’t provide an API for, but I would advice against that.

        A good writeup on inheritance VS composition can be found here:
        http://www.javaworld.com/javaworld/jw-11-1998/jw-11-techniques.html
        it’s from 1998 but just as good today.

        My short summary would be that by inheriting ListBox you expose the API of that class to your users. That means that you can never choose to use any other widget for your fancy EnumListBox widget in the future (withouth breaking your API). By creating a composite instead you only expose as much of the underlying API as you choose to and you can freely switch the underlying widget later. Or create a FilteredListBox that also has a TextBox for entering a filter string.

        If you start thinking about the different kinds of viewers you can create you’ll see that most of them behave in the same way. Whether you display your list of boolean values (in this case)

        as a single select listbox (with one value being true at a time)

        or as a group of radio buttons

        or as a multiselect list (all selected are true)

        or as a group of check boxes

        the only thing separating the class ListBoxViewer from GroupViewer (for lack of a better name) would be what widgets they choose to use. They can both inherit SelectableListViewer which holds most of the logic and a setMultiSelect(boolean enabled) method.

        Later on when you want to create a table with a check box on each row that selects the row for some future action it’s just more of the same old.

        This would not be possible if your view inherited ListBox and the group of check boxes would not be possible at all without composition.

      • OK, I see the point. We’re just talking about reuse at a different layer. My presenter only knows about the widget via the HasValue interface, so I can drop in any widget that implements it in an alternate view. I agree that I could better enforce this by extending Composite and implementing only the methods in HasValue.

      • perwiklander said

        Yep, I think that gives better reuse. And I am toying with the idea of creating some widgets that might get used by others and then it gets more important to think about the API of the widgets. Other users might not even be doing MVP but still want to use a good ListBoxViewer.

  2. Greg said

    This is slightly old, but I still have found it useful. Do have one question though. Is there anyway to allow GWT to create the widget from the definition in a uiBinder file or must you use @UiField(provided = true).

    Cheers

    • Thanks, Greg. Yes, you have to use provided=true because EnumListBox as shown here doesn’t have a no-arg constructor. However, you could create one and move the existing initialization code from the constructor to a new method.

  3. Greg said

    One more thing – have you ever tried assigning an EnumListBox from the ui template to a field declared as HasValue? For me, GWT complains with Template field and owner field types don’t match: com.tendril.ui.core.widget.client.EnumListBox != com.google.gwt.user.client.ui.HasValue

    I believe this is caused by HasValue – when its determining if fields can be assigned it doesn’t want to assign HasValue to HasValue

  4. Jeroen Wolff said

    Hi, i’m trying to use EnumListbox in UiBinder and i have to give it a Class<T extends Enum> in the UiBinder files, how can i do that?

    • There’s no way to specify a parameterized type in a uiBinder.xml, but you can specify the type in Java and use provided=true in the xml file. FYI, CellList and SelectionModel in GWT 2.1 have eliminated the need for EnumListBox.

      • Jeroen Wolff said

        David, thanks. I have done it with provided=true. But than i have problems with @UIHandler(). The onValueChange() signature doesn’t match my method….
        But i can’t see how CellList can help me. What i want is a or some other kind of listbox that opens as the moment the user selects it. How can CellList eliminate EnumListBox?

        Thanks , Jeroen

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: