Creating Reusable GUI Components

As a kind of atomic building blocks W4 Toolkit provides reusable widgets suitable to build rich Web GUIs. With W4T Eclipse you get a powerful tool for composing UIs based on these widgets. Now one of the common problems when building GUIs is that the widgets placed at the GUIs component tree often depend on (dynamic) model data. This makes the use of a GUI builder awkward, since e.g. the content of a table cannot be assembled at design time.

This tutorial covers how to create reusable UI components (with W4T Eclipse) which are based on model data. It shows some strategies to separate model and view layers, avoid code redundancy and ease maintenance of the UI.

As an example we will create a tree viewer component. To have a look at the source code of the final implementations click here.


Setting up the Stage

Data models exists in various forms and the data pool of a system normally changes during runtime. But models frequently consist of recurrent structures and patterns, a one-to-many association between two entity types for example. The design goal of building reusable UI components should be to handle these recurrent structures. Once you have a solution for the pattern, you can re-use it in every subsequent implementation. Continuing our one-to-many association example - the many part is often realized as a java.util.Collection - we could think of a kind of table viewer working on that collection.

But how can we build UI components without polluting the model layer with widget classes and vice versa? A convenience approach is to create an adapter [Gof] for the base widget which fits the needs. Instead of dealing directly with the model data in this adapter we use a kind of mediator [Gof]. The mediator defines the communication protocol between the adapter and the model. As a result every class implementing the mediators interface could be used to provide content to the widget adapter.

To make things a little bit more tangible let's start with our example. At first we take a look at the data model for which we want to create our example UI component:

The model is a simple variety of the composite pattern [Gof]. To visualize data entities based on the composite pattern the use of the TreeView widget is obvious. We create a userdefined component with the New WebPanel Wizard representing our widget adapter. In our example we use the WebGridLayout as layout manager but there are various possibilities to create specific layouts. The important point is that you add the TreeView widget (our wrapped component) to the container. Since our component is a viewer for trees we will call it TreeViewer.

After that we have to think about the mediator. We will realize it as an interface declaring the methods we need for building up a tree. Since the implementing classes will provide our TreeViewer with the content of a underlying model we call it ITreeContentProvider:

package example.viewer;

public interface ITreeContentProvider {
  
  public Object getRoot();
  
  public Object[] getChildren( Object parent );
  
  public boolean hasChildren( Object parent );
  
  public boolean isLeaf( Object element );
}
You may be a little surprised, but that's all we need to build up the tree structure in the TreeViewer. But before we go into detail explaining the usage of the ITreeContentProvider there are some preliminaries to do. First create a userdefined code block in your TreeViewer. To keep a distinction between the W4T Eclipse generated part and the tree building part we create a class which is used as delegate called from the TreeViewer. Create the getter and setter methods for the ITreeContentProvider in the TreeViewer class:
  //$userdefined_start
  private TreeViewerDelegate delegate;
  
  private TreeViewerDelegate getDelegate() {
    if( delegate == null ) {
      delegate = new TreeViewerDelegate( treeView );
    }
    return delegate;
  }
  
  public void setContentProvider( final ITreeContentProvider contentProvider ) {
    getDelegate().setContentProvider( contentProvider );
  }
  
  public ITreeContentProvider getContentProvider() {
    return getDelegate().getContentProvider();
  }  
  //$userdefined_end
As you see our delegate is instantiated with a reference to the wrapped TreeView (the delegate is instantiated with 'lazy loading' to ensure the TreeView parameter has been initialized). The idea is to rebuild the tree every time the content provider changes. Now we have a look at the most important code fragments that use the content provider for building up the tree structure (TreeViewerDelegate):
  private void createTree() {
    Object element = contentProvider.getRoot();
    TreeNode root = ( TreeNode )createItem( element );
    map.put( root, element );
    addItem( treeView, root );
    createLevel( root );
  }

  private void createLevel( final Item parent ) {
    if( contentProvider.hasChildren( map.get( parent ) ) ) {
      Object[] elements = contentProvider.getChildren( map.get( parent ) );
      for( int i = 0; i < elements.length; i++ ) {        
        Item item  = createItem( elements[ i ] );
        map.put( item, elements[ i ] );
        addItem( ( TreeNode )parent, item );
        createLevel( item );
      }    
    }
  }
  
  private Item createItem( final Object element ) {
    Item result = null;
    if( contentProvider.isLeaf( element ) ) {
      result = new TreeLeaf();
    } else {
      result = new TreeNode();
    }
    return result;
  }
The starting point is the createTree() method. We read the root content out of the content provider. Since the W4 Toolkit TreeView API distinguishes two tree components (TreeLeaf, TreeNode) we use the createItem() method to build the fitting tree item depending on the result of the isLeaf() method of the content provider. Since events triggered by user input occur on the widgets we need to map the model data to the tree items. If for example a leaf is selected, it is easy to get the corresponding data which belongs to that leaf.

Note: Mapping the models data is not a mixture between the model layer and the view layer, since we reference the data as simple Object types and not with their specific types. We treat the data as kind of black box!

The rest is quite easy: The addItem() methods simply inserts the root node into the tree. After that we go into a recursion building up the tree. We iterate over the children of each element of the underlying model, creating tree items for each, map the elements to the items and put the items into the tree.


Visualize the Model Data

As you may have recognized we are not totally done, because something important is missing. We have built a widget structure representing the models data. But running the example will show a tree without any labels on the items!

But how can we get the information we want to display without access to the data object types? The answer is the same as above. We use another interface that declares the methods we need to get our label information from the underlying data. We could also decide to create one big mediator treating all in once, but I prefer having small pieces with clear-cut responsibilities. Since the job of this interface is to provide the viewer with the label information to display, we call it for usage in our TreeViewer example ITreeLabelProvider:

package example.viewer;

public interface ITreeLabelProvider {
  
  public String getLabel( Object element );
  
  public String getImageSetName( Object element );
}
As you may want to change the images representing the datas item in the tree view, there is also a method for dealing with the tree items image set attribute. We extend the implementation of the createItem() method as follows:
  private Item createItem( final Object element ) {
    Item result = null;
    String imageSetName = labelProvider.getImageSetName( element );
    if( contentProvider.isLeaf( element ) ) {
      result = new TreeLeaf();
      ( ( TreeLeaf )result ).setImageSetName( imageSetName );
    } else {
      result = new TreeNode();
      ( ( TreeNode )result ).setImageSetName( imageSetName );
    }
    result.setLabel( labelProvider.getLabel( element ) );
    return result;
  }
To keep things simple we start with a very simple implementation as an inner class of our delegate:
  public class DefaultLabelProvider implements ITreeLabelProvider {

    public String getLabel( final Object element ) {
      return element.toString();
    }

    public String getImageSetName( final Object element ) {
      return treeView.getImageSetName();
    }   
  }
This is quite self-explanatory. We rely on the data elements to provide a meaningfull toString() implementation which is used as label (how to handle other cases will be covered later).


Implementing the Content Provider

The implementation of the data model abstraction layer with our content provider follows the JavaBeans standard, in particular it must provide a public and parameterless constructor to work with W4T Eclipse!

Let's have a look at a content provider implementation of our ITreeContentProvider example:

package example;

import example.data.*;
import example.viewer.ITreeContentProvider;

public class SimpleContentProvider implements ITreeContentProvider {

  private Composite root;

  public SimpleContentProvider() {
    // ...
    // create the data model
    // ...
  }  
  public Object getRoot() {
    return root;
  }
  public Object[] getChildren( final Object parent ) {
    Object[] result = null; 
    if( parent instanceof Composite ) {
      result = ( ( Composite )parent ).getChildren();
    }
    return result;
  }
  public boolean hasChildren( final Object parent ) {
    boolean result = false;
    if( parent instanceof Composite ) {
      result = getChildren( parent ).length > 0; 
    }
    return result;
  }
  public boolean isLeaf( final Object element ) {
    return element instanceof Leaf;
  }
}
It is important to see that the implementation uses the data model types but does not use any widget type.

We are now ready to test our UI component. Add the TreeViewer to the widget selector. Create a WebForm for testing the component. Change to the design mode of the editor and place our TreeViewer widget at the form. Use the property view to select the content provider:

Launching the example WebForm will result in something like this:




Advanced Concepts

As we mentioned above our default label provider relies on the toString() implementation of the data entities. This is not a good choice, because the data element should not determine its representation. To demonstrate another implementation of the ITreeLabelProvider we change our data model a little bit:

After we added the value attribute to the Leaf the toString() implementation may not fit our needs for the label representation anymore. Another issue may be to use different images for leaves with certain properties:

package example;

import example.data.Leaf;
import example.viewer.ITreeLabelProvider;

public class SimpleLabelProvider implements ITreeLabelProvider {

  public String getLabel( final Object element ) {
    String result = "";
    if( element instanceof Leaf ) {
      Leaf leaf = ( Leaf )element;
      result = leaf.getName() + ": " + leaf.getValue();
    } else {
      result = element.toString();
    }
    return result;
  }
  public String getImageSetName( final Object element ) {
    String result = "images/treeview/modern/Modern";
    if( element instanceof Leaf ) {
      Leaf leaf = ( Leaf )element;
      if( leaf.getValue().startsWith( "1" ) ) {
        result = "images/treeview/swing/Swing";
      }
    } 
    return result;
  }
}
Again use the property view to select the label provider. After relaunching the demo form the result looks like this:

Note: we have done some changes to an entity of the data model, we created an appropriate label provider, but we had not to change anything of our TreeViewer implementation.

Another common task of GUI components working with model data is to change the sort order of the data representation or to use filters for hiding data blocks temporarily. You can solve both problems with the same strategie we used before. We will only demonstrate the filter as example:

package example.viewer;

public interface ITreeFilter {
  
  public Object[] filter( Object[] elements );
  
}
And here comes the implementation:
package example;

import java.util.ArrayList;
import example.viewer.ITreeFilter;

public class SimpleTreeFilter implements ITreeFilter {
  
  public Object[] filter( final Object[] elements ) {
    ArrayList filtered = new ArrayList();
    if( elements != null ) {
      for( int i = 0; i < elements.length; i++ ) {
        // filter all elements which contain the character '2' in its String representation
        if( elements[ i ].toString().indexOf( "2" ) == -1 ) {
          filtered.add( elements[ i ] );
        }
      }      
    }
    Object[] result = new Object[ filtered.size() ];
    filtered.toArray( result );
    return result;
  }
}
Add a field to the delegate and supplement its createLevel() method with the following snippet:
  private void createLevel( final Item parent ) {
    if( contentProvider.hasChildren( map.get( parent ) ) ) {
      Object[] elements = contentProvider.getChildren( map.get( parent ) );
      if( filter != null ) {
        elements = filter.filter( elements );
      }
      for( int i = 0; i < elements.length; i++ ) {        
        Item item  = createItem( elements[ i ] );
        map.put( item, elements[ i ] );
        addItem( ( TreeNode )parent, item );
        createLevel( item );
      }    
    }
  }
Finally forward the setter and getter method to the TreeViewer. Again use the property view to select the Filter for the TreeViewer. After relaunching the demo form the result looks like this (Note: the tree is fully expanded!):




Working with Widget Selection

Last but not least most of the time you don't want to simply present your data, you will have to react on selection of certain data elements. As mentioned above a user does not directly select the data, he selects its representation widget. Therefore its a good choice to use the widgets event handler as a starting point for the widget adapter. Normally you can pass the listener (by delegating the add- and removeXXXListener method) you need from the widget to the adapter. Provided that a mapping between the representing widget instance and its data object exists, you can retrieve the data in the event handler method from the widget adapter.

The implementation in our example is self-explanatory, so if you want to have a look at the code click here.

You can use the event view to add an action handler to our TreeViewer. Its job is to write the String representation of the selected data into a WebLabel:

  //$userdefinedEventListener_start
  private void doTreeViewer1WebActionPerformed( WebActionEvent e ) throws Exception {
    String value = treeViewer1.getElement( ( Item )e.getSource() ).toString();
    wlbSelection.setValue( value );
  }
  //$userdefinedEventListener_end

Summary

References: