Tuesday, July 13, 2010

Customizing Which Form Fields Are Displayed: Part 1

Talking to Dan Allen about Metawidget recently, he commented:

"One thing that I think would really help people along is to have an example of how to customize form fields displayed on a JSF view. This seems to be one of the first thing any JSF developer wonders about. I know that customization is possible, both at the global and field level, but just having a simple how-to would go a long way"

This is a great point, worthy of a little blog series.

Where To Start: InspectionResultProcessors

Out of the box, Metawidget has a few different options for ordering fields. I'll just mention the most simple ones here.

You can exclude fields on a 'per screen' basis using stub tags:

<m:metawidget value="#{person}">
   <m:stub value="#{person.age}"/>
</m:metawidget>

You can order fields at the 'domain' level using annotations:

package com.myapp;

import org.metawidget.inspector.annotation.*;

public class Person {
   public String name;

   @UiComesAfter( "name" )
   public int age;

   @UiComesAfter( "age" )
   public boolean retired;
}

And you can use XML (which is implicitly ordered):

<entity type="com.myapp.Person">
   <property name="name"/>
   <property name="age"/>
   <property name="retired"/>
</entity>

But after lots of feedback from interviews, adoption studies and forum posts, I realized the issue of 'what fields appear, and what order they appear in' covered a lot of different preferences and requirements. To satisfy these, I introduced the InspectionResultProcessor interface (click to enlarge):


InspectionResultProcessors sit after the Inspectors and before the WidgetBuilders. Out of the box, UiComesAfter is implemented using ComesAfterInspectionResultProcessor. But InspectionResultProcessors have access both to the inspection result and the Metawidget that is about to render it, and this vantage point gives them a number of capabilites.

Swing: Letting The Screen Decide

One capability is to allow the screen to choose which fields it should render. Now, I don't particularly recommend this approach: it means your screen contains hard-coded field names. These won't refactor well, nor will they evolve well as your business objects evolve. But, hey, Metawidget is all about working the way you want to!

So let's do a Swing example first as it's easier to cut and paste and try yourself. Here's a custom InspectionResultProcessor that chooses, and sorts, business object fields based on a JComponent client property. It extends the code from the Metawidget Tutorial:

package com.myapp;
         
import static org.metawidget.inspector.InspectionResultConstants.*;

import javax.swing.*;
import org.metawidget.swing.*;
import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.util.*;
import org.w3c.dom.*;


public class Main {

   public static void main( String[] args ) {
      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.addInspectionResultProcessor( new IncludingInspectionResultProcessor() );
      metawidget.putClientProperty( "include", new String[]{ "retired", "age" } );

      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }
   
   static class Person {
      public String name;
      public int age;
      public boolean retired;
   }
   
   static class IncludingInspectionResultProcessor
      implements InspectionResultProcessor<SwingMetawidget> {
   
      public String processInspectionResult( String inspectionResult, SwingMetawidget metawidget, Object toInspect, String type, String... names ) {
   
         String[] includes = (String[]) metawidget.getClientProperty( "include" );
         Document document = XmlUtils.documentFromString( inspectionResult );
         Element entity = (Element) document.getDocumentElement().getFirstChild();      
         int propertiesToCleanup = entity.getChildNodes().getLength();

         // Pull out the names in order

         for( String include : includes ) {
         
            Element property = XmlUtils.getChildWithAttributeValue( entity, NAME, include );

            if ( property == null )
               continue;

            entity.appendChild( property );
            propertiesToCleanup--;
         }

         // Remove the rest

         for( int loop = 0; loop < propertiesToCleanup; loop++ ) {
            entity.removeChild( entity.getFirstChild() );
         }

         return XmlUtils.documentToString( document, false );
      }
   }

}

If this approach happens to be your preference, you may be surprised you have to code an InspectionResultProcessor for it yourself - why doesn't Metawidget support it out of the box? However, you may also be surprised at how many other preferences there are, as we shall see later in this blog series. Metawidget isn't about providing flags for every possible variation: UI requirements are too diverse for that. Instead, Metawidget tries to be pluggable enough, in enough places, that you can always tweak it to suit.

JSF: Letting The Screen Decide

For completeness (and because it's what Dan actually asked for!) let's do a JSF version of the above:

package com.myapp;
         
import static org.metawidget.inspector.InspectionResultConstants.*;

import javax.faces.component.*;
import org.metawidget.faces.*;
import org.metawidget.faces.component.*;
import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.util.*;
import org.w3c.dom.*;

public class IncludingInspectionResultProcessor
   implements InspectionResultProcessor<UIMetawidget> {
   
   public String processInspectionResult( String inspectionResult, UIMetawidget metawidget, Object toInspect, String type, String... names ) {

      UIParameter includeParameter = FacesUtils.findParameterWithName( metawidget, "include" );
   
      if ( includeParameter == null )
         return null;

      String[] includes = ArrayUtils.fromString( (String) includeParameter.getValue() );
      Document document = XmlUtils.documentFromString( inspectionResult );
      Element entity = (Element) document.getDocumentElement().getFirstChild();      
      int propertiesToCleanup = entity.getChildNodes().getLength();

      // Pull out the names in order

      for( String include : includes ) {
      
         Element property = XmlUtils.getChildWithAttributeValue( entity, NAME, include );
      
         if ( property == null )
            continue;

         entity.appendChild( property );
         propertiesToCleanup--;
      }

      // Remove the rest

      for( int loop = 0; loop < propertiesToCleanup; loop++ ) {
         entity.removeChild( entity.getFirstChild() );
      }

      return XmlUtils.documentToString( document, false );
   }
}

You'd then add this into your metawidget.xml:

<metawidget xmlns="http://metawidget.org"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://metawidget.org http://metawidget.org/xsd/metawidget-1.0.xsd" version="1.0">

   <htmlMetawidget xmlns="java:org.metawidget.faces.component.html">
      .
      .
      .
      <inspectionResultProcessors>
         <array>
            <includingInspectionResultProcessor xmlns="java:com.myapp"/>
         </array>
      </inspectionResultProcessors>
      .
      .
      .
   </htmlMetawidget>
</metawidget>

And use it in your page:

<m:metawidget value="#{contact.current}">
   <f:param name="include" value="title,firstname,surname,edit,save,delete"/>
</m:metawidget>

Note this can include actions (like 'edit' and 'save') as well as properties. More InspectionResultProcessor examples to come!

0 comments: