Child pages
  • GWT widget definitions
Skip to end of metadata
Go to start of metadata

Introduction

This page contains a general proposal of how to define GWT widgets for the Geomajas GWT Client 2. The rules laid out here should cover map controls as well as any other widget. Our goal is to follow the GWT best practices as much as possible, while keeping usability in mind.

Requirements

Defining a widget is not unlike defining any other API: it should be clear what it is and how to use it. Additional requirements for widgets is that their layout should be easily customized. We're not talking a few colors here and there, but the entire HTML construct of a widget should be customizable as well as it's CSS.

In short the following requirements should be met:

  • All widgets that are defined in plugins should be part of the API (@Api annotation). We should never create widgets that are not meant to be used.
  • The style of a widget should be easily customized. The more ways there are to customize the style, the better.
  • The HTML construct of a widget should be customizable. This means we need a clean separation between the GUI definition and the actual code (MVC, MVP, ...)
  • We should follow GWT best practices as much as possible. This should make it easier for others to understand our code.
  • All widgets should support i18n.

Proposal

General definition

Most of the above requirement can be met by making use of the GWT ClientBundles as much as possible:

  • By using Messages or Constants the widgets should support i18n.
  • By using ResourceBundles and CssResourceBundles we can provide a way for people to overwrite the default style. This only works though if the resource bundles are not initialized in a static object. Resource bundles should instead be provided through a constructor. It would be best to declare resources in a widget as final.
  • By using the GWT UiBinder mechanism, one can provide a HTML construct for a widget. As with the normal resource bundles, the UiBinder instance should not be defined as a static, but as a final object. It should be provided through the constructors.

For every widget, we should define a specific resource bundle. In case some styles or images are often used, a CommonResource bundle should exist as well (could be per plugin). Still not every developer may want to make use of resource bundles to overwrite  a widgets style. An an extra effort we should make sure that all CSS classes used in resource bundles have a fixed name. By disabling the CSS classname obfuscation, people can still provide their own CSS file the old-fashioned way.

For most widgets this would mean that at least the following files should be provided:

  • Widget class (.java)
  • Default UiBinder construct (.ui.xml)
  • Resource bundle combining images and CSS.
  • Css Resource bundle
  • Css file
  • Images

I would propose we create a package per widget and place all the above files in that package save for the images. For images I would create an "image" sub-folder.

Another important aspect is that for every widget we should provide a specific example in the showcase;

To separate the view from the presentation logic, the MVP pattern should be used whenever it makes sense (a sufficient amount of testable logic in the presenter). This pattern has some advantages:

  • the view can be replaced by an entirely different implementation (even in another technology like SmartGWT)
  • the view becomes easier to maintain as its code can be run separately (you can even make an entry point that shows all the views of your application)
  • by replacing the view with a mock, the code becomes unit testable

File names

Let's say we want to create a new widget by the name of "ExampleWidget". In that case, the other files should use the following names:

  • ExampleWidget.java
  • ExampleWidgetView.java
  • ExampleWidgetViewImpl.java
  • ExampleWidgetViewImpl.ui.xml
  • ExampleWidgetPresenter.java
  • ExampleWidgetPresenterImpl.java
  • ExampleWidgetResource.java
  • ExampleWidgetCssResource.java
  • example-widget.css

Example

Using the ExampleWidget above, it's code would look something like this:

/**
 * Example widget.
 * 
 * @author Jan De Moerloose
 * 
 */
public class ExampleWidget implements IsWidget { 

	private ExampleWidgetView view;

	private ExampleWidgetPresenter presenter;

	public ExampleWidget() {
		this(Example.getInstance().getViewFactory().createExampleWidgetView());
	}

	public ExampleWidget(ExampleWidgetView view) {
		this.view = view;
		presenter = new ExampleWidgetPresenterImpl(view);
	}

	public Widget asWidget() {
		return view.asWidget();
	}
}

As you can see, the widget contains a view and a presenter, encapsulating the MVP implementation from the framework user. The view is by default obtained from a factory (see further), allowing easy replacement by the framework user.

The view is passed to the constructor of the presenter and the IsWidget() implementation delegates to the view.

The view code (interface and implementation) would look something like this:

/**
 * MVP view.
 * 
 * @author Jan De Moerloose
 * 
 */
public interface ExampleWidgetView extends IsWidget {
    /**
     * Sets the presenter for callback.
     * 
     * @param presenter
     */
    void setPresenter(ExampleWidgetPresenter presenter);

    void sayHello();
}


/**
 * Implementation of {@link ExampleWidgetView}.
 * 
 * @author Jan De Moerloose
 * 
 */
public class ExampleWidgetViewImpl implements ExampleWidgetView {

    @UiField
    protected Button button;

    private ExampleWidgetPresenter presenter;

    private SimpleLayoutPanel widget;

    private static final MyUiBinder UIBINDER = GWT.create(MyUiBinder.class);
    /**
     * UI binder interface.
     * 
     * @author Jan De Moerloose
     * 
     */
    interface MyUiBinder extends UiBinder<Widget, ExampleWidgetViewImpl> {
    }

    public ExampleWidgetViewImpl() {
        this(Example.getInstance().getBundleFactory().createExampleWidgetResource());
    }

    public ExampleWidgetViewImpl(ExampleWidgetResource resource) {
        resource.css().ensureInjected();
        widget = (SimpleLayoutPanel) UIBINDER.createAndBindUi(this);
        button.setText("Hello");
    }

    public void setPresenter(ExampleWidgetPresenter presenter) {
        this.presenter = presenter;
    }

    @UiHandler("button")
    public void onButtonClicked(ClickEvent event) {
        presenter.onButton();
    }

    public void sayHello() {
        Window.alert("Hello !");
    }

    public Widget asWidget() {
        return widget;
    }
}

Note that the resource bundle or the UiBinder do not have to be declared as final objects. If you do not need them in any of your methods, just use them in the constructor without saving them to a field.

The presenter is passed to the view as a callback interface. The view is essentially passive and should only delegate actions to the presenter.

The client resource bundle is by default obtained from a factory (see further), allowing easy replacement by the framework user.

The presenter code (interface and implementation) would look something like this:

/**
 * MVP presenter = callback interface for view.
 * 
 * @author Jan De Moerloose
 * 
 */
public interface ExampleWidgetPresenter {
    public void onButton();
}

/**
 * Implementation of {@link ExampleWidgetPresenter}.
 * 
 * @author Jan De Moerloose
 * 
 */
public class ExampleWidgetPresenterImpl implements ExampleWidgetPresenter {

    private ExampleWidgetView view;

    public ExampleWidgetPresenterImpl(ExampleWidgetView view) {
        this.view = view;
        this.view.setPresenter(this);
    }

    public void onButton() {
        view.sayHello();
    }
}

The presenter implementation takes the view as a constructor argument. The same constructor can be used for unit testing. The view interface has a setPresenter() method, which is called by the presenter in its constructor to connect the view with the presenter.

Factories and common plugin code

We define singleton factories for the client resource bundles and the views of all widgets. Singletons are obtained from the main plugin singleton, which has the plugin's name but is not its entry point.

File names

If the plugin's name is example, the following files should be defined (in the top client package):

  • Example.java (main singleton)
  • ExampleEntryPoint.java
  • ExampleClientBundleFactory
  • ExampleViewFactory

The main singleton would look as follows:

public class Example {

    private static Example instance;

    private ExampleViewFactory viewFactory;

    private ExampleClientBundleFactory bundleFactory;

    private Example() {
    }

    public static Example getInstance() {
        if (instance == null) {
            instance = new Example();
        }
        return instance;
    }

    public ExampleClientBundleFactory getBundleFactory() {
        if (bundleFactory == null) {
            bundleFactory = GWT.create(ExampleClientBundleFactory.class);
        }
        return bundleFactory;
    }

    public ExampleViewFactory getViewFactory() {
        if (viewFactory == null) {
            viewFactory = GWT.create(ExampleViewFactory.class);
        }
        return viewFactory;
    }
    
}

The factories can be replaced through deferred binding

The factories look like this:

public class ExampleViewFactory {

    public ExampleWidgetView createExampleWidgetView() {
        return new ExampleWidgetViewImpl();
    }
}

public class ExampleClientBundleFactory {

    public ExampleWidgetResource createExampleWidgetResource() {
        return GWT.create(ExampleWidgetResource.class);
    }
}

No-style resources

To allow overriding of the style, a version of the client bundle has to be created that has empty css classes. The following files are involved:

  • ExampleclientBundle
  • ExampleWidgetResourceNoStyle.java
  • example-widget-nostyle.css

ExampleWidgetResourceNoStyle replaces ExampleWidgetResource but refers to example-widget-nostyle.css, which is the same as example-widget.css, but with empty css classes. The end result is an application with empty css classes baked in, allowing web developers to override the css in the normal way.

Unit Testing

Unit testing the presenter is easy because the view can be mocked. We use the GWT mockito framework for this (see https://github.com/google/gwtmockito).

For our example, a unit test would look as follows:

import static org.mockito.Mockito.*;

@RunWith(GwtMockitoTestRunner.class)
public class ExampleWidgetPresenterImplTest {

    @Mock
    private ExampleWidgetView view;

    @Test
    public void onButton() {
        ExampleWidgetPresenterImpl presenter = new ExampleWidgetPresenterImpl(view);
        presenter.onButton();
        verify(view).sayHello();
    }
}

The mockito framework allows this and much more:

  • mocking of classes and interfaces
  • specifying return values for mocked methods
  • capturing arguments of mocked methods (useful if you want to mock a callback or make a more complex argument verification
  • verifying the mocked methods are called with the correct arguments (using matchers)

Replacing the default style through GWT

To replace the default style, define another implementation for the client resource bundle factory through deferred binding:

    <replace-with class="myproject.MyExampleClientBundleFactory">
        <when-type-is class="example.client.ExampleClientBundleFactory" />
    </replace-with>

MyExampleClientBundleFactory should implement the same methods but use different css files and images.

Replacing the default style through css

To replace the default style through css, define the nostyle implementation for the client resource bundle factory through deferred binding:

    <replace-with class="example.client.nostyle.ExampleClientBundleFactoryNoStyle">
        <when-type-is class="example.client.ExampleClientBundleFactory" />
    </replace-with>

ExampleClientBundleFactoryNoStyle is an implementation that provides empty css classes for all the css used (as we explained in the no-style paragraph). By importing a css file at the web page level, the web developer can now define his or her own style for each of these classes. We could already provide a css file with all the default classes for this purpose, so the web developer can use the default style as a starting point.

Replacing the view completely

To replace the default style completely, define another implementation for the view factory through deferred binding:

    <replace-with class="myproject.MyExampleViewFactory">
        <when-type-is class="example.client.ExampleViewFactory" />
    </replace-with>

Sample code

A github project with sample code has been set up:

https://github.com/geomajas/geomajas-widget-definitions-sample

To test, clone the repo and run the usual in the example directory:

mvn install jetty:run

  • No labels

6 Comments

  1. Pieter, 

    I am missing MVP in this? Wouldn't it be better if we add ExampleWidgetPresenter.java to the list of files?

    1. First of all, I'm no big fan of MVP. As noone seems to be using it, I'm obviously not the only one. It is often very cumbersome to do simple things. Your code also explodes.

      But it has the advantage of being able to separate the GUI from the workings behind that GUI. I consider the XML files to be the view while the java file is your controller/presenter. You can always provide different XML files, or use widgets from different libraries in your XML files. I'm not sure if you can implement the UiBinder without using an XML file though.

      Personally I think using MVP scares people off. I know of no widget libraries that use it.

      1. I don't know how we are going to do unit testing without separation. 

        Our users are not obliged to use mvp if we use mvp.

        Let's discuss this tonight.

  2. Are there best practices for style names? 

    I always use gm-stylename, to avoid collisions. Maybe this should be documented as well.

  3. If you use gm-stylename, you can use wildcards to externalize all styles:

    @external gm-*;

  4. I have tried to complete the documentation (including MVP) and added a sample (standalone) to github.

    Could you please review and comment ?