Sunday, October 31, 2010

Metawidget Neat Trick: Configuring JSF Programmatically

Here's a third neat trick I've discovered while using Metawidget, the Java form generator, in my own work.

Say you need to support multiple configurations for multiple Metawidgets in your app. For example, you may need one Metawidget on your page for inspecting properties but not actions, and another for inspecting just actions. The latter might be placed in a toolbar or something.

You can see a working example of this in the JSF PenguinColony Demo. It defines 2 configurations, metawidget.xml and metawidget-action.xml, and chooses between them using Metawidget's config attribute:

<m:metawidget value="#{penguin.current}"/>
   ...
<m:metawidget value="#{penguin}" config="metawidget-action.xml"/>

But there is another way: using JSF's binding attribute. Here you can defer all configuration into Java code - just as you might with, say, SwingMetawidget or AndroidMetawidet:

<m:metawidget binding="#{penguin.metawidget}"/>

Then you have the full power of Java to make decisions around how to configure each Metawidget for different scenarios. For example:

public class PenguinBean {

   public UIMetawidget getMetawidget() {

      // First-time init
      //
      // JSF spec: "When a component instance is first created (typically by virtue of being
      // referenced by a UIComponentELTag in a JSP page), the JSF implementation will retrieve the
      // ValueExpression for the name binding, and call getValue() on it. If this call returns a
      // non-null UIComponent value (because the JavaBean programmatically instantiated and
      // configured a component already), that instance will be added to the component tree that
      // is being created"

      UIMetawidget metawidget = new HtmlMetawidget();
      initMetawidget( metawidget );
      return metawidget;
   }

   public void setMetawidget( UIMetawidget metawidget ) {

      // POST-back init
      //
      // JSF spec: "When a component tree is recreated during the Restore View phase of
      // the request processing lifecycle, for each component that has a ValueExpression
      // associated with the name 'binding', setValue() will be called on it, passing the
      // recreated component instance"

      initMetawidget( metawidget );
   }
   
   private void initMetawidget( UIMetawidget metawidget ) {
   
      ...configure Metawidget programmatically...
   }   
}

This (little used) feature of JSF is a great way to 'open up' the programmatic API of Metawidget inside your JSF applications.

Feedback welcome!

Monday, October 25, 2010

Safely manipulating the component tree with JSF 2, revisited

I've been working with both the Apache MyFaces and the Oracle Mojarra teams on improving JSF 2 support in Metawidget.

Metawidget is a more dynamic component than most, and exercises JSF 2 in a way few component libraries do. In particular, it stresses the relationship between dynamically modifying the component tree, partial state saving (new in JSF 2), and firing nested SystemEvents (new in JSF 2). As such, all 3 teams (MyFaces, Mojarra, Metawidget) have uncovered bugs in our implementations.

I'm delighted to say it looks like all these will be resolved in time for MyFaces 2.0.3 and Mojarra 2.2. For those interested in the 'correct' implementation of a dynamic JSF 2 component, as agreed by all teams, I've put together a little Acid Test that tests your JSF implementation for full compliance. In the process, it demonstrates the 'right' way to implement a dynamic JSF 2 component using SystemEvents. Which is:

public class UIAddComponent
   extends UIComponentBase implements SystemEventListener {

   public UIAddComponent() {

      FacesContext context = FacesContext.getCurrentInstance();
      UIViewRoot root = context.getViewRoot();

      root.subscribeToViewEvent( PreRenderViewEvent.class, this );
   }

   public boolean isListenerForSource( Object source ) {

      return ( source instanceof UIViewRoot );
   }

   public void processEvent( SystemEvent event )
      throws AbortProcessingException {

      if ( !FacesContext.getCurrentInstance().isValidationFailed() ) {
      
         // Safely manipulate component tree here
      }
   }
}

My thanks to all teams for working so hard on this issue!

UPDATE: looks like this approach may be making it into the JSF spec: http://java.net/jira/browse/JAVASERVERFACES_SPEC_PUBLIC-1007

Saturday, October 23, 2010

JavaBean Convention: Relating Public Getters/Setters To Private Fields

The upcoming v1.05 release of Metawidget, the pragmatic User Interface generator, includes support for annotating private fields. Take the following example:

public class Person {

   @Id
   private int mId;

   @Column( nullable = false )
   private String mName;

   private int mAge;

   public String getName() {
      return mName;
   }

   public void setName( String name ) {
      mName = name;
   }

   @UiComesAfter( "name" )
   public String getAge() {
      return mAge;
   }

   public void setAge( int age ) {
      mAge = age;
   }
}

Here we have some JPA annotations (@Id, @Column) on our private fields and some UI annotations (@UiComesAfter) on our public getters/setters. This is quite a common situation, but it's very hard to support because the JavaBean specification doesn't define a relationship between public getters/setters and which private field they relate to. This is pretty obvious when you consider that some getters/setters don't have any private field. For example, a getAge method might calculate its value based on getDateOfBirth rather than have an mAge field per se.

So how do JPA, and other frameworks like Hibernate Validator, support this? Well, they cheat: using reflection to set the fields directly. But this doesn't work for most UI technologies, such as Swing's BeansBinding, or JSF, or Spring. Most UI technologies expect publically accessible getters/setters.

So we need to key off public getters/setters, but we want to annotate private fields. Implementations like GroovyPropertyStyle and ScalaPropertyStyle support this nicely, because those environments do define a mapping between getter/setter and private field. But JavaBeans do not. So how can JavaBeanPropertyStyle support it?

Well, let's be pragmatic: although not enforced, most developers adopt some kind of convention for how their getters/setters are named versus how their private fields are named. There are variations, but we can make this configurable. For v1.05 I've adopted a simple approach based on MessageFormat. So:

  • {0} (eg. dateOfBirth, surname)

  • 'm'{1} (eg. mDateOfBirth, mSurname)

  • 'm_'{0} (eg. m_dateOfBirth, m_surname)
Here's a complete example:

package com.myapp;

import java.text.MessageFormat;
import java.util.List;

import javax.swing.JFrame;

import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.impl.propertystyle.javabean.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.SwingMetawidget;
import org.metawidget.util.CollectionUtils;

public class Main {

   public static void main( String[] args ) {

      // Model

      Person person = new Person();

      // Metawidget

      SwingMetawidget metawidget = new SwingMetawidget();
      JavaBeanPropertyStyleConfig propertyStyleConfig = new JavaBeanPropertyStyleConfig();
      propertyStyleConfig.setPrivateFieldConvention( new MessageFormat( "'m'{1}" ) );

      BaseObjectInspectorConfig inspectorConfig = new BaseObjectInspectorConfig();
      inspectorConfig.setPropertyStyle( new JavaBeanPropertyStyle( propertyStyleConfig ) );
      metawidget.setInspector( new CompositeInspector(
            new CompositeInspectorConfig().setInspectors(
                  new PropertyTypeInspector( inspectorConfig ),
                  new MetawidgetAnnotationInspector( inspectorConfig ) )));
      metawidget.setToInspect( person );

      // Frame

      JFrame frame = new JFrame( "Example" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   /**
    * Model
    */

   public static class Person {

      private String         mName;

      @UiComesAfter( "name" )
      private int            mAge;

      @UiComesAfter( "age" )
      private boolean         mRetired;

      @UiComesAfter( "retired" )
      private List<Address>   mAddresses   = CollectionUtils.newArrayList();

      @UiLarge
      @UiComesAfter( "addresses" )
      private String         mNotes;

      public String getName() {

         return mName;
      }

      public void setName( String name ) {

         mName = name;
      }

      public int getAge() {

         return mAge;
      }

      public void setAge( int age ) {

         mAge = age;
      }

      public boolean isRetired() {

         return mRetired;
      }

      public void setRetired( boolean retired ) {

         mRetired = retired;
      }

      public List<Address> getAddresses() {

         return mAddresses;
      }

      public void setAddresses( List<Address> addresses ) {

         mAddresses = addresses;
      }

      public String getNotes() {

         return mNotes;
      }

      public void setNotes( String notes ) {

         mNotes = notes;
      }
   }

   public static class Address {

      private String   mStreet;

      @UiComesAfter( "street" )
      private String   mCity;

      @UiComesAfter( "city" )
      private String   mState;

      public Address( String street, String city, String state ) {

         mStreet = street;
         mCity = city;
         mState = state;
      }

      public String getStreet() {

         return mStreet;
      }

      public void setStreet( String street ) {

         mStreet = street;
      }

      public String getCity() {

         return mCity;
      }

      public void setCity( String city ) {

         mCity = city;
      }

      public String getState() {

         return mState;
      }

      public void setState( String state ) {

         mState = state;
      }
   }
}

Hopefully this will work for most use cases. For those needing more control, consider extending JavaBeanPropertyStyle and overriding getPrivateField.

Feedback welcome!

Friday, October 22, 2010

Metawidget: Collections Support

Update: this blog entry has been superseded. Metawidget has a Swing CollectionTableModel built-in as of version 3.7

I was recently asked...

"Is there a way to have all [Collection] references rendered as a link that pops up to a list with search/filter/select options?"

This comes up quite a bit. The problem, which is implied in the question, is there's a lot of aesthetic preferences involved in rendering Collections. The poster was asking for a pop-up box with search/filter/select options. But what about those who want to edit the Collection in-place? Or have an edit button on each row that you have to click? Or want their Collection to be paginated? Sortable? Etc etc.

To support this, Metawidget provides WidgetBuilders.

Here's an example of a custom WidgetBuilder that wires up a Swing JTable. This isn't part of the core Metawidget distribution, because there's lots of personal preferences here - not least because Swing doesn't have a standard TableModel for rendering Collections. But the following should give you a good start in writing your own WidgetBuilder to suit your tastes (other examples you can look at include HtmlWidgetBuilder.createDataTableComponent and DisplayTagWidgetBuilder):

package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.util.*;

import javax.swing.*;
import javax.swing.table.*;

import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.java5.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.*;
import org.metawidget.swing.widgetbuilder.*;
import org.metawidget.util.*;
import org.metawidget.widgetbuilder.composite.*;
import org.metawidget.widgetbuilder.iface.*;
import org.w3c.dom.*;

public class Main {

   @SuppressWarnings( "unchecked" )
   public static void main( String[] args ) {

      // Model

      Person person = new Person();
      person.getAddresses().add( new Address( "Street 1", "City 1", "State 1" ) );
      person.getAddresses().add( new Address( "Street 2", "City 2", "State 2" ) );

      // Metawidget

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setInspector( new CompositeInspector(
            new CompositeInspectorConfig().setInspectors(
                  new PropertyTypeInspector(),
                  new MetawidgetAnnotationInspector(),
                  new Java5Inspector() ) ) );
      metawidget.setWidgetBuilder( new CompositeWidgetBuilder<JComponent, SwingMetawidget>(
            new CompositeWidgetBuilderConfig<JComponent, SwingMetawidget>().setWidgetBuilders(
                  new CollectionWidgetBuilder(),
                  new SwingWidgetBuilder() ) ) );
      metawidget.setToInspect( person );

      // Frame

      JFrame frame = new JFrame( "Example" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   /**
    * Model
    */

   public static class Person {

      private String         mName;

      private int            mAge;

      private boolean         mRetired;

      private List<Address>   mAddresses   = CollectionUtils.newArrayList();

      private String         mNotes;

      public String getName() {

         return mName;
      }

      public void setName( String name ) {

         mName = name;
      }

      @UiComesAfter( "name" )
      public int getAge() {

         return mAge;
      }

      public void setAge( int age ) {

         mAge = age;
      }

      @UiComesAfter( "age" )
      public boolean isRetired() {

         return mRetired;
      }

      public void setRetired( boolean retired ) {

         mRetired = retired;
      }

      @UiComesAfter( "retired" )
      public List<Address> getAddresses() {

         return mAddresses;
      }

      public void setAddresses( List<Address> addresses ) {

         mAddresses = addresses;
      }

      @UiLarge
      @UiComesAfter( "addresses" )
      public String getNotes() {

         return mNotes;
      }

      public void setNotes( String notes ) {

         mNotes = notes;
      }
   }

   public static class Address {

      private String   mStreet;

      private String   mCity;

      private String   mState;

      public Address( String street, String city, String state ) {

         mStreet = street;
         mCity = city;
         mState = state;
      }

      public String getStreet() {

         return mStreet;
      }

      public void setStreet( String street ) {

         mStreet = street;
      }

      @UiComesAfter( "street" )
      public String getCity() {

         return mCity;
      }

      public void setCity( String city ) {

         mCity = city;
      }

      @UiComesAfter( "city" )
      public String getState() {

         return mState;
      }

      public void setState( String state ) {

         mState = state;
      }
   }

   static class CollectionWidgetBuilder
      implements WidgetBuilder<JComponent, SwingMetawidget> {

      public JComponent buildWidget( String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) {

         // Not for us?

         if ( TRUE.equals( attributes.get( HIDDEN ) ) || attributes.containsKey( LOOKUP ) ) {
            return null;
         }

         String type = attributes.get( TYPE );

         if ( type == null || "".equals( type ) ) {
            return null;
         }

         final Class<?> clazz = ClassUtils.niceForName( type );

         if ( clazz == null ) {
            return null;
         }

         if ( !List.class.isAssignableFrom( clazz ) ) {
            return null;
         }

         // Inspect type of List

         String componentType = attributes.get( PARAMETERIZED_TYPE );
         String inspectedType = metawidget.inspect( null, componentType, (String[]) null );

         // Determine columns

         List<String> columns = CollectionUtils.newArrayList();
         Element root = XmlUtils.documentFromString( inspectedType ).getDocumentElement();
         NodeList elements = root.getFirstChild().getChildNodes();

         for ( int loop = 0, length = elements.getLength(); loop < length; loop++ ) {

            Node node = elements.item( loop );
            columns.add( metawidget.getLabelString( XmlUtils.getAttributesAsMap( node ) ) );
         }

         // Fetch the data. This part could be improved to use BeansBinding or similar

         List<?> list = (List<?>) ClassUtils.getProperty( metawidget.getToInspect(), attributes.get( NAME ) );

         // Return the JTable

         @SuppressWarnings( "unchecked" )
         ListTableModel<?> tableModel = new ListTableModel( list, columns );

         return new JScrollPane( new JTable( tableModel ) );
      }
   }

   static class ListTableModel<T>
      extends AbstractTableModel {

      private List<T>         mList;

      private List<String>   mColumns;

      public ListTableModel( List<T> list, List<String> columns ) {

         mList = list;
         mColumns = columns;
      }

      public int getColumnCount() {

         return mColumns.size();
      }

      @Override
      public String getColumnName( int columnIndex ) {

         if ( columnIndex >= getColumnCount() ) {
            return null;
         }

         return mColumns.get( columnIndex );
      }

      public int getRowCount() {

         return mList.size();
      }

      public T getValueAt( int rowIndex ) {

         if ( rowIndex >= getRowCount() ) {
            return null;
         }

         return mList.get( rowIndex );
      }

      public Object getValueAt( int rowIndex, int columnIndex ) {

         if ( columnIndex >= getColumnCount() ) {
            return null;
         }

         T t = getValueAt( rowIndex );

         if ( t == null ) {
            return null;
         }

         return ClassUtils.getProperty( t, getColumnName( columnIndex ) );
      }
   }
}

Wednesday, October 20, 2010

Form Generator: Method Behind The Madness

I thought I'd post some of the academic papers and articles I've written, to make it easier for those outside academic institutions to access them. While Metawidget is very focused on being a practical form generator, there's a lot of theoretical background underpinning it:

Hope you find them interesting reading! Feedback most welcome!

Monday, October 18, 2010

GridBagLayout honoring PreferredSize

I was recently asked to provide an example of a Swing Metawidget layout where some JComponents had a preferredSize. By default org.metawidget.swing.layout.GridBagLayout just stretches everything as wide as possible, but sometimes this is not what you want.

Complete example below. Some points of note:
  • The example includes a CustomWidgetBuilder that chooses JTextField instead of JSpinner for int fields. Note you shouldn't do this by extending SwingWidgetBuilder! You should just write a lightweight WidgetBuilder for the widget you're interested in, and return null for everything else. Leave CompositeWidgetBuilder to chain all the WidgetBuilders together

  • There's a CustomWidgetProcessor that sets all JTextFields to the same preferredSize. It leave other components (such as JCheckBox and JTextArea) alone

  • There's a CustomLayout that tweaks org.metawidget.swing.layout.GridBagLayout to honor preferredSize
Hope that helps!

package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.awt.*;
import java.util.Map;

import javax.swing.*;

import org.metawidget.inspector.annotation.*;
import org.metawidget.swing.SwingMetawidget;
import org.metawidget.swing.layout.GridBagLayout;
import org.metawidget.swing.widgetbuilder.SwingWidgetBuilder;
import org.metawidget.widgetbuilder.composite.*;
import org.metawidget.widgetbuilder.iface.WidgetBuilder;
import org.metawidget.widgetprocessor.iface.WidgetProcessor;

public class Main {

   @SuppressWarnings( "unchecked" )
   public static void main( String[] args ) {

      // Model

      Person person = new Person();

      // Metawidget

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setWidgetBuilder( new CompositeWidgetBuilder<JComponent, SwingMetawidget>(
            new CompositeWidgetBuilderConfig<JComponent, SwingMetawidget>().setWidgetBuilders(
                  new CustomWidgetBuilder(),
                  new SwingWidgetBuilder() ) ) );
      metawidget.addWidgetProcessor( new CustomWidgetProcessor() );
      metawidget.setMetawidgetLayout( new CustomLayout() );
      metawidget.setToInspect( person );

      // Frame

      JFrame frame = new JFrame( "Example" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   /**
    * Model
    */

   public static class Person {

      private String   mName;
      private int      mAge;
      private boolean   mRetired;
      private Address   mAddress   = new Address();
      private String   mNotes;

      public String getName() {

         return mName;
      }

      public void setName( String name ) {

         mName = name;
      }

      @UiComesAfter( "name" )
      public int getAge() {

         return mAge;
      }

      public void setAge( int age ) {

         mAge = age;
      }

      @UiComesAfter( "age" )
      public boolean isRetired() {

         return mRetired;
      }

      public void setRetired( boolean retired ) {

         mRetired = retired;
      }

      @UiComesAfter( "retired" )
      public Address getAddress() {

         return mAddress;
      }

      public void setAddress( Address address ) {

         mAddress = address;
      }

      @UiLarge
      @UiComesAfter( "address" )
      public String getNotes() {

         return mNotes;
      }

      public void setNotes( String notes ) {

         mNotes = notes;
      }
   }

   static class Address {

      private String   mStreet;
      private String   mCity;
      private String   mState;

      public String getStreet() {

         return mStreet;
      }

      public void setStreet( String street ) {

         mStreet = street;
      }

      @UiComesAfter( "street" )
      public String getCity() {

         return mCity;
      }

      public void setCity( String city ) {

         mCity = city;
      }

      @UiComesAfter( "city" )
      public String getState() {

         return mState;
      }

      public void setState( String state ) {

         mState = state;
      }
   }

   /**
    * Custom WidgetBuilder.
    * <p>
    * You don't have to extend <code>SwingWidgetBuilder</code>. You just build the one widget
    * you're interested in, and return null for all the rest. Rely on
    * <code>CompositeWidgetBuilder</code> to chain WidgetBuilders together.
    */

   static class CustomWidgetBuilder
         implements WidgetBuilder<JComponent, SwingMetawidget> {

      public JComponent buildWidget( String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) {

         if ( "int".equals( attributes.get( TYPE ) ) ) {
            return new JTextField();
         }

         return null;
      }
   }

   /**
    * Custom WidgetProcessor.
    * <p>
    * Tweak similar widgets based on their type. For example, set all <code>JTextFields</code> to
    * the same preferred size.
    */

   static class CustomWidgetProcessor
      implements WidgetProcessor<JComponent, SwingMetawidget> {

      public JComponent processWidget( JComponent component, String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) {

         if ( component instanceof JTextField ) {
            component.setPreferredSize( new Dimension( 100, 20 ) );
         }

         return component;
      }
   }

   /**
    * Custom Layout.
    * <p>
    * Use the new <code>setFillConstraints</code> hook to avoid filling width if
    * <code>preferredSize</code> has been set.
    */

   static class CustomLayout
      extends GridBagLayout {

      @Override
      protected void setFillConstraints( JComponent component, GridBagConstraints componentConstraints ) {

         if ( component.getPreferredSize().getWidth() == 100 ) {
            return;
         }

         super.setFillConstraints( component, componentConstraints );
      }
   }
}

Wednesday, October 13, 2010

Metawidget: JRadioButton instead of JComboBox

I've had a couple requests from people wanting to use JRadioButtons in their generated UI (by default, Metawidget generates JComboBox components for selection lists).

Devil in the Details

There are a few aesthetic issues with using JRadioButtons: should they be arranged horizontally or vertically; should they be horizontal up to a certain number of items, then switch to vertical; should they have a choice at the top for 'null'; should you be able to 'unselect all' after you've made an initial choice; should they only be used for not-nullable fields; etc. These are the kind of things keeping me from putting JRadioButtons in the core SwingWidgetBuilder.

But it's straightforward to add your own WidgetBuilder - where you can decide all the above little UI aesthetics according to your personal taste.

In a Bind

While you're at it, you probably want to add binding support. Again this depends on your personal preference, but let's say you want to use BeansBindingProcessor. It turns out BeansBinding isn't very good at binding to groups of JRadioButtons (a known problem), so one approach is to dummy up a little ButtonGroupPanel with its own getter and setter.

Example Code

Here's an example WidgetBuilder. You'll need to add it as part of a CompositeWidgetBuilder, as shown here in the Reference Documentation.

public class JRadioButtonWidgetBuilder
   implements WidgetBuilder<JComponent, SwingMetawidget>, SwingValuePropertyProvider {

   public JComponent buildWidget( String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) {

      String lookupAttribute = attributes.get( LOOKUP );

      if ( lookupAttribute == null ) {
         return null;
      }

      String[] lookups = ArrayUtils.fromString( lookupAttribute );
      String[] lookupLabels = ArrayUtils.fromString( attributes.get( LOOKUP_LABELS ) );

      ButtonGroupPanel panel = new ButtonGroupPanel();
      panel.setLayout( new GridLayout( 1, lookups.length ) );

      for ( int loop = 0; loop < lookups.length; loop++ ) {

         JRadioButton radioButton = new JRadioButton();

         if ( lookupLabels.length == 0 ) {
            radioButton.setText( lookups[loop] );
         } else {
            radioButton.setText( lookupLabels[loop] );
         }

         radioButton.setActionCommand( lookups[loop] );
         panel.add( radioButton );
      }

      return panel;
   }

   public String getValueProperty( Component component ) {

      if ( component instanceof ButtonGroupPanel ) {
         return "selected";
      }

      return null;
   }

   public static class ButtonGroupPanel
      extends JPanel {

      private ButtonGroup   mButtonGroup   = new ButtonGroup();

      public String getSelected() {

         ButtonModel buttonModel = mButtonGroup.getSelection();

         if ( buttonModel == null ) {
            return null;
         }

         return buttonModel.getActionCommand();
      }

      public void setSelected( String selected ) {

         for ( Enumeration<AbstractButton> e = mButtonGroup.getElements(); e.hasMoreElements(); ) {

            AbstractButton button = e.nextElement();

            if ( !selected.equals( button.getActionCommand() ) ) {
               continue;
            }

            String oldValue = getSelected();
            mButtonGroup.setSelected( button.getModel(), true );
            firePropertyChange( "selected", oldValue, selected );
            break;
         }
      }

      @Override
      protected void addImpl( Component component, Object constraints, int index ) {

         super.addImpl( component, constraints, index );

         if ( component instanceof AbstractButton ) {
            mButtonGroup.add( (AbstractButton) component );
         }
      }
   }
}

Feedback welcome!

Tuesday, October 12, 2010

Metawidget: Adjusting the Style and Layout of Fields

I was recently asked:

"The one thing I'm still missing is information about how to adjust the style and layout of the fields. In addition to field ordering, I think this is the one major concern people always have. Early on, they will likely just throw up the fields on the page however they fall, but over time, they will want to tweak it. What level of control do they have here? Explaining this customization closes a very important gap in people's minds"

There are 3 parts to the answer:

#1: Metawidget doesn't try to 'own' your entire UI
Metawidget, as its name suggests, is just a widget. It's not trying to be your whole UI. It just focuses on creating native subcomponents for slotting into existing UIs. And it's very lightweight, so there's nothing stopping you combining 4 or 5 or more Metawidgets on the same screen:

Here you can see a simple UI with its Metawidgets outlined with red squares. There are 5 Metawidgets used here. You can scatter them around in an arbitrary fashion, embed them inside other parts of your UI (like dialog boxes and status bars), and surround them however you want (like images next to them, or JTables under them).

This approach alone gives you a huge amount of flexibility in how you design the look of your UI, and ensures you're not locked in to any particular appearance and your app doesn't 'feel' like it is using UI generation.

#2: Metawidget doesn't hide your existing UI toolkit
Most UI frameworks have extensive support for tweaking the look of your UI. For example Swing has Look & Feels; JSF has CSS and skinnable component libraries; Android has XML style files.

Metawidget doesn't hide any of these. It just creates the native components for you, and you can style them like you always do. It helps a little where it can, for example by putting CSS style classes where you want them and generating sensible ids for table rows. Here's some actual output from the JSF Metawidget:

<table id="form:j_id_jsp_628835842_3" class="table-form">
   <tfoot>
      <tr>
         <td colspan="3" class="buttons">
            <input id="form:contactSearchSearch" name="form:contactSearchSearch" type="submit" value="Search" />
            <input id="form:contactSearchAddPersonal" name="form:contactSearchAddPersonal" type="submit" value="Add Personal Contact" />
            <input id="form:contactSearchAddBusiness" name="form:contactSearchAddBusiness" type="submit" value="Add Business Contact" />
         </td>
      </tr>
   </tfoot>
   <tbody>
      <tr id="table-contactSearchCurrentFirstname-row">
         <th id="table-contactSearchCurrentFirstname-label-cell" class="table-label-column">Firstname:</th>
         <td id="table-contactSearchCurrentFirstname-cell" class="table-component-column"><input id="form:contactSearchCurrentFirstname" name="form:contactSearchCurrentFirstname" type="text" value="" /></td>
         <td class="table-required-column"><div></div></td>
      </tr><tr id="table-contactSearchCurrentSurname-row">
         <th id="table-contactSearchCurrentSurname-label-cell" class="table-label-column">Surname:</th>
         <td id="table-contactSearchCurrentSurname-cell" class="table-component-column"><input id="form:contactSearchCurrentSurname" name="form:contactSearchCurrentSurname" type="text" value="" /></td>
         <td class="table-required-column"><div></div></td>
      </tr><tr id="table-contactSearchCurrentType-row">
         <th id="table-contactSearchCurrentType-label-cell" class="table-label-column">Type:</th>
         <td id="table-contactSearchCurrentType-cell" class="table-component-column">
            <select id="form:contactSearchCurrentType" name="form:contactSearchCurrentType" size="1">
               <option value="" selected="selected"></option>
               <option value="PERSONAL">Personal</option>
               <option value="BUSINESS">Business</option>
            </select>
         </td>
         <td class="table-required-column"><div></div></td>
      </tr>
   </tbody>
</table>

Not very shocking, and that's kind of the point. This is familiar territory. The same approach your UI toolkit already uses. Oh, and it's fully configurable of course.

#3: Metawidget has pluggable layouts
The one part of your UI Metawidget does control is how the subcomponents inside each Metawidget are laid out. But this is completely pluggable. Metawidget comes with pre-built layouts for, say, arranging the widgets in a table (with one column for the label and another for the widget) or arranging the widgets horizontally all in a row (handy for button bars).

It also comes with a bunch of 'layout decorators', so you can decorate one layout with another. For example, you can decorate a HtmlTableLayout with a RichFaces TabPanelLayoutDecorator to put parts of your UI inside tabs:

And finally Metawidget makes it straightforward to plug in your own, custom Layouts as needed. See this section of the Reference Documentation.