A Groovy Vaadin Builder

Simply Building Vaadin UIs

Overview

Tree shaped data and object structures can be constructed in Groovy relatively easy with so-called Builder classes. This is based on Groovy’s ability to catch and handle all method calls with its MetaObject protocol. In this way, pseudo-method calls can be handled that don’t lead to a method with this name but create a new object, based on the used method name. See details at the Groovy Documentation site. Groovy provides several classes to support implementation of your own buildes, i.e. BuilderSupport and FactoryBuilderSupport. You can find a few tutorials for these on the web.

The VaadinBuilder class featured here is a subclass of BuilderSupport. It implements some special features to ease constructing complex Vaadin UIs.

  • All supported Vaadin components are defined with two enums to support code completion in modern IDEs.
    • enum C holds Vaadin layout containers, enum F input field components
    • using import static de.geobe.util.vaadin.VaadinBuilder.C (resp. F), one can access VaadinBuilder pseudeomethods using Groovy’s literal method calling syntax object."methodname"(...) and GStrings [i.e. builder."$C.hlayout" (...) { ... }]. Typos are thus largely avoided. Some examples are following furter below.
  • All created components are recorded in a map, using a given name as key. If no name is given, they are automatically numbered (label1, label2, …). Names can get a prefix to easily distinguish between similar components in different UI areas (i.e. different tab pages).
  • Different subtrees of Vaadin components can be constructed separately and assembled centrally. Complex UIs can thus be easily implemented in separate classes (i.e. one class per tab or accordeon page).
  • Vaadin menus (MenuBar and MenuBar.MenuItem objects) can be integrated into the UI with the Builder.
  • All setXxx() and addXxx() methods with none, one or arbitrary (…) parameters of Vaadin components can be called with the builder. Event handlers are added using Closures. So additional configuration code is widely aoided.

Usage

A simple example building a dialog to capture new scrum tasks:

class SubtaskDialog {
    TextField tag, estimate, spent
    TextArea description
    CheckBox supertask, completed
    Button saveButton, cancelButton
    Window window

    private VaadinBuilder winBuilder = new VaadinBuilder()

    public Window build() {
        String keyPrefix = "${subkeyPrefix}dialog."
        winBuilder.keyPrefix = keyPrefix
        window = winBuilder."$C.window"('Create Subtask', [spacing: true, 
                                                            margin : true,
                                                            modal  : true,
                                                           closable: false]) {
            "$C.vlayout"('top', [spacing: true, margin: true]) {
                "$F.text"('Task', [uikey: TAG])
                "$C.hlayout"('Status', [spacing: true, margin: false]) {
                    "$F.checkbox"('superordinate', [uikey: IS_SUPERTASK])
                    "$F.checkbox"('completed', [uikey: IS_COMPLETED])
                }
                "$F.text"('Estimate', [uikey: ESTIMATE])
                "$F.text"('Time used', [uikey: SPENT])
                "$F.textarea"('Description', [uikey: DESCRIPTION])
                "$C.hlayout"([uikey: 'buttonfield', spacing: true]) {
                    "$F.button"('Cancel', [uikey         : 'cancelbutton', 
                                           disableOnClick: true,
                                           enabled       : true,
                                           clickListener : {
                                               sm.execute(Event.Cancel) }])
                    "$F.button"('Save', [uikey         : 'savebutton', 
                                         disableOnClick: true,
                                         enabled       : true,
                                         clickListener : {
                                             sm.execute(Event.Save) }])
                }
            }
        }

        def dialogComponents = winBuilder.uiComponents
        tag = dialogComponents."$keyPrefix$TAG"
        estimate = dialogComponents."$keyPrefix$ESTIMATE"
        spent = dialogComponents."$keyPrefix$SPENT"
        description = dialogComponents."$keyPrefix$DESCRIPTION"
        completed = dialogComponents."$keyPrefix$IS_COMPLETED"
        supertask = dialogComponents."$keyPrefix$IS_SUPERTASK"
        saveButton = dialogComponents."${keyPrefix}savebutton"
        cancelButton = dialogComponents."${keyPrefix}cancelbutton"
        window.center()
    }
}
Comments on the code
  • [line 2 – 6]
    Declaring fields for all relevant components that have to be accessed programmatically. Groovy’s normal access rules apply.
  • [line 8]
    Here we are constructing a new builder instance just for this dialog window. Later examples will show how a single builder instance is passed through to different classes to build a more complex UI.
  • [line 11]
    The keyPrefix parameter is not really necessary because the builder instance is used only locally.
  • [from line 13]
    Here, the UI is built. The build pseudomethods are called with the following parameters, all optional:

    • A String (caption), used by most Vaadin components.
    • A Map of key/value pairs, configuring the component.
    • Layout (=container) components take a block of code (that is a Closure), in which contained components are constructed.
  • Keys of the Map are mostly (with a few exceptions) related to set… or add… methods of the component as defined in the Vaadin-API definiert sind.
    • modal: true in line 15 calls setModal(true) of calss Window
    • clickListener: { sm.execute(Event.Save) } calls addClickListener(...) of class Button and passes the Closure as parameter.
    • Exceptions and specials:
       
      /** key of the component in the component map */
      public static final String UIKEY = 'uikey'
      /** gridPosition is set in the parent component */
      public static final String GRID_POSITION = 'gridPosition'
      /** alignment is set in the parent component */
      public static final String ALIGNMENT = 'alignment'
      
      • uikey assigns a name to a component for the uiComponents Map.
      • gridPosition and alignment in Vaadin are properties of the superordinate layout container (in contrast to Swing and other UI frameworks). To have a better readable configuration, VaadinBuilder takes these properties with the component and propagates them to the parent container transparently.
      • Parameterless set… methods (a pecularity of Vaadin) have to be configured with a null value.
  • [line 42]
    The Window-Position cannot be configured with the builder because it related method window.center() does not adhere to the set… or add… pattern.

More complex UIs with several subtrees

The pseudomethod subtree can be used to compose a complex UI from several subtrees of components. For this purpose, a VaadinBuilder object is created in a Vaadin UI class. It is then passed to the additional classes that each build a part of the UI. As an example we have a UI with a HorizontalSplitPanel as top level layout, holding a TreeView in the left pane and a TabSheet with several tabs on the right.

va4spbuild01

The TreeView as well as all single Tabs are implemented in their own class. Thus separation of concerns is well achieved. Each class handles a different aspect of the UI.

Top level UI class

In class ScrumView.groovy all component subtrees are initialized and inserted into the layout component of the top level.

package de.fh_zwickau.pti.geobe.view

import com.vaadin.annotations.Theme
import com.vaadin.server.VaadinRequest
import com.vaadin.spring.annotation.SpringUI
import com.vaadin.ui.Component
import com.vaadin.ui.TabSheet
import com.vaadin.ui.UI
import de.geobe.util.vaadin.VaadinBuilder
import de.fh_zwickau.pti.geobe.util.VaadinSelectionKeyListener
import org.springframework.beans.factory.annotation.Autowired

import static VaadinBuilder.C
import static VaadinBuilder.F

/**
 * The main view class for Scrum UI
 * Created by georg beier on 16.11.2015.
 */
@SpringUI(path = "")
@Theme("valo")
class ScrumView extends UI implements VaadinSelectionKeyListener {

    def VaadinBuilder vaadin
    def widgets = [:]
    @Autowired
    private ProjectTab projectTab
    @Autowired
    private TaskTab taskTab
    @Autowired
    private SprintTab sprintTab
    @Autowired
    ProjectTree projectTree

    private Component root, projectSelectTree, 
            projectSubtree, sprintSubtree, taskSubtree

    @Override
    protected void init(VaadinRequest request) {
        setContent(initBuilder())
        initComponents()
    }

    /**
     * Construction of the Vaadin component tree
     *     "branches" are created before the "trunk" and then added with subtree
     * @return
     */
    Component initBuilder() {
        vaadin = new VaadinBuilder()
        projectSelectTree = projectTree.buildSubtree(vaadin, 'menutree.')
        projectSubtree = projectTab.buildSubtree(vaadin, 'project.')
        sprintSubtree = sprintTab.buildSubtree(vaadin, 'sprint.')
        taskSubtree = taskTab.buildSubtree(vaadin, 'task.')

        root = vaadin."$C.hsplit"([uikey: 'topsplit', splitPosition: 20.0f]) {
            "$F.subtree"(projectSelectTree, [uikey: 'menu'])
            "$C.tabsheet"([uikey: 'tabs']) {
                "$F.subtree"(projectSubtree, [uikey: 'projectpanel'])
                "$F.subtree"(sprintSubtree, [uikey: 'sprintpanel'])
                "$F.subtree"(taskSubtree, [uikey: 'taskpanel'])
            }
        }
        widgets = vaadin.uiComponents
//        def wtree = vaadin.toString()
//        println wtree
        root
    }

    private initComponents(){
        // subviews must be initialized only after composition of the whole UI
        projectTree.init()
        projectTab.init()
        sprintTab.init()
        taskTab.init()
        projectTree.selectionModel.addKeyListener(this)
    }
/**
 * is fired when an entry of a selection component was selected
 * @param event id of the selected element, normally its domain class itemId
 */
    @Override
    void onItemKeySelected(Map<String, Serializable> itemId) {
        TabSheet tabs = widgets['tabs']
        switch (itemId['type']) {
            case 'Project':
            case 'project':
                tabs.selectedTab = projectSubtree
                break
            case 'Task':
            case 'task':
                tabs.selectedTab = taskSubtree
                break
            case 'Sprint':
            case 'sprint':
                tabs.selectedTab = sprintSubtree
                break
        }
    }
}
Comments on the code
20 – 22
The top level UI class is a normal Vaadin UI class. Accordingly URL-Path and Theme are annotated and class extends Vaadins UI base class.
26 – 33
The subordinate UI components are implemented as Spring managed beans and @Autowired. Thus objects are managed by Spring and can communicate among each other.
39 – 42
The Vaadin init() method as first action builds the component tree including the subtrees. Then the subordinate UI components are initialized. This sequence is important, else one would try to initialize not yet constructed objects.
50
The VaadinBuilder for the whole component tree is created.
51 – 54
Subtrees are built. It is assumed that their classes provide a method buildSubtree(...) which gets the builder object and a prefix String as parameters. Using different prefixes assures that all subtree components have diffrent keys in the component map. This realizes a poor mans namespaces.
56 – 61
Here the UI is composed. A SplitPanel gets the tree component as first (= left) subcomponent. The second (= right) subcomponent is a TabSheet containing three subtrees as its tab components.
64-66
Components created by the VaadinBuilder are stored in a Map (widgets).
A test printout that prints the component tree as ASCII graphic is commented out. This can be helpful to check the UI structure. [TODO: Method will be renamed to avoid error messages in the debugger]
70
Now the subordinate objects with the subtrees can be initialized.
76, 83 …
This is an implementation of the VaadinSelectionKeyListener Interface callback method: A KeyListener is registered and implemented. Click events in the tree component eventually trigger a change of the selected tab.

SubTree – a Base Class for Implementing Component Subtrees

Class SubTree.groovy is meant as a base class for component subtrees that provides common functionality and defines an implementation template.

package de.geobe.util.vaadin

import com.vaadin.ui.Component
import com.vaadin.ui.UI

/**
 * A base class for building Vaadin component subtrees with VaadinBuilder
 * Created by georg beier on 16.11.2015.
 */
abstract class SubTree {
    /**
     * builder is configured here and used in subclasses
     */
    protected VaadinBuilder vaadin
    /**
     * make prefix available for accessing components in the subclasses
     */
    protected String subkeyPrefix
    protected def uiComponents

    /**
     * set component prefix in builder, delegate building subtree to subclass
     * and reset prefix afterwards.
     * @param builder   VaadinBuilder instance that builds the whole GUI
     * @param componentPrefix   name prefix for components in this subtree
     * @return  topmost component (i.e. root) of this subtree
     */
    Component buildSubtree(VaadinBuilder builder, String componentPrefix) {
        this.vaadin = builder
        def oldKeyPrefix = builder.getKeyPrefix()
        subkeyPrefix = oldKeyPrefix + componentPrefix
        builder.setKeyPrefix subkeyPrefix
        Component component = build()
        builder.setKeyPrefix oldKeyPrefix
        component
    }

    /**
     * build component subtree.
     * @return  topmost component (i.e. root) of subtree
     */
    abstract Component build()

    /**
     * initialize subtree components. should be called after whole component tree is built.
     * call sequence of different subtrees may be important.
     * @param value various parameters needed for initialization
     */
    void init(Object... value) {}

    protected setComponentValue(String id, Object value) {
        uiComponents."${subkeyPrefix + id}".value = value.toString()
    }

    protected setComponentValue(String id, Boolean value) {
        uiComponents."${subkeyPrefix + id}".value = value
    }

    protected UI getVaadinUi(Component c) {
        Component parent = c?.parent
        if(parent instanceof UI) {
            parent
        } else {
            getVaadinUi(parent)
        }
    }

    protected Long longFrom(String val) {
        try {
            new Long(val)
        } catch(NumberFormatException e) {
            0L
        }
    }
}
comments on code
28
This method sets a naming prefix in VaadinBuilder that is put in front of all component names. This makes it easy to have some kind of namespaces for subtrees. Building the component tree is delegated to a subclass. Finally, the naming prefix is reset.
33 and 42
Here the real build() call happens that has to be implemented in subclasses.
49
The initializing method init(…) generally can not be called before all subtrees are built. For example, listeners on component events in other subtrees can be created here.

A Tab Page as an Example for a Component Subtree

Class ProjectTree.groovy manages a tree view of domain classes, shown in the left part of the UI.

import ...

import static de.geobe.util.vaadin.VaadinBuilder.C
import static de.geobe.util.vaadin.VaadinBuilder.F

/**
 * Main selection component is this tree view that represents relevant
 * objects and associations in the domain model
 * Created by georg beier on 17.11.2015.
 */
@SpringComponent
@UIScope
class ProjectTree extends SubTree
        implements Serializable {

    public static final String PROJECT_TYPE = 'Project'
    public static final String SPRINT_TYPE = 'Sprint'
    public static final String TASK_TYPE = 'Task'

    private static final String PTREE = 'ptree'
    private static final String MENU = 'logoutmenu'
    private Tree projectTree
    private VaadinTreeHelper treeHelper

    private uiComponents

    private Map<String, Serializable> selectedProjectId

    def getSelectedProjectId() { selectedProjectId }

    @Autowired
    private ProjectService projectService

    @Autowired
    private VaadinSecurity vaadinSecurity

    VaadinSelectionModel selectionModel = new VaadinSelectionModel()

    @Override
    Component build() {
        vaadin."$C.vlayout"() {
            "$F.menubar"([uikey: MENU]) {
                "$F.menuitem"('Logout', [command: { vaadinSecurity.logout() }])
            }
            "$C.panel"('Projekte', [spacing: true, margin: true]) {
                "$F.tree"('Projekte, Backlogs und Sprints',
                        [uikey: PTREE, caption: 'MenuTree',
                        valueChangeListener: {treeValueChanged(it)}])
            }
        }
    }

    @Override
    void init(Object... value) {
        uiComponents = vaadin.uiComponents
        projectTree = uiComponents."${subkeyPrefix + PTREE}"
        treeHelper = new VaadinTreeHelper(projectTree)
        buildTree(projectTree)
    }

    /**
     * handle changes of tree selection
     * @param event info on the newly selected tree item
     */
    private void treeValueChanged(Property.ValueChangeEvent event) { ... }

    /**
     * build a tree representing the domain model
     * @param projectTree
     */
    private void buildTree(Tree projectTree) { ... }

    /**
     * disable the tree while a tree item is edited on one of the tab pages
     */
    public void onEditItem() { ... }

    /**
     * enable and update the tree after editing an item
     * @param itemId identifies edited item
     * @param caption eventually updated caption of the edited item
     * @param mustReload tree must reload after new item was created 
     *        or structure changed
     */
    public void onEditItemDone(Object itemId, String caption, boolean mustReload = false) { ... }
}
comments on code
22, 23
The class is anotated as a @SpringComponent and thus available as a Spring bean. The bean scope is bound to the Vaadin session with the @UIScope annotation. This means that for every VaadinSession there is exactly one bean of this type that can be bound with @Autowired.
51
Here the subtree is built.
53-55
A VaadinMenu with a single Logout entry is built above the TreeView.
59, 76
The Listener for the TreeView is assigned directly in the VaadinBuilder as a Closure which delegates the more extensive implementation to the treeValueChanged(...) method.
65
The init() method is quite short. Most of the initialization is either directly coded in the builder or delegated to the TreeHelper object.
76, 82, 87, 96
These methods are described on a separate page in context of using Vaadin Tree objects. [TODO: Translate into English]

Adding More Components to a VaadinBuilder Object

Many additional UI components exist for Vaadin, both free and commercial. Furthermore it could be necessary to extend available UI classes, i.e. to implement the Interface View for a Navigator. To support these additional components, VaadinBuilder provides the methods addCustomField(String name, String fqn) and addCustomContainer(String name, String fqn) to add additional IO elements and layout container.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.