Wednesday, July 14, 2010

Customizing Which Form Fields Are Displayed: Part 2

Following on from Part 1, Dan asked whether the screen could decide based on some kind of 'view groups', rather like Bean Validation's validation groups.

I actually like this idea a lot: it combines the flexibility of local field ordering with the safety of not hard-coding field names into the screens. Having said that, this is the first time it's been suggested. So I'll wait and see if it becomes popular before deciding whether to provide it 'out of the box' (this blog series will explore a lot of alternate preferences).

In the meantime, example implementation below. Some points to note:
  • It's in Swing so you can just cut and paste and run it

  • It defines a custom annotation (UiViewGroup) and custom Inspector (ViewGroupInspector) to detect it

  • It defines a ViewGroupInspectionResultProcessor that screens out properties/actions

  • It uses this in a chain with the usual ComesAfterInspectonResultProcessor
To see it in action, try running the code and changing the 'putClientProperty' line to use different view groups (ie. 'summary' or 'detail').

package com.myapp;

import java.lang.annotation.*;
import java.util.*;

import javax.swing.*;

import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.inspectionresultprocessor.sort.*;
import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.*;
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.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
         new PropertyTypeInspector(),
         new MetawidgetAnnotationInspector(),
         new ViewGroupInspector() )));
      metawidget.addInspectionResultProcessor( new ViewGroupInspectionResultProcessor() );
      metawidget.addInspectionResultProcessor( new ComesAfterInspectionResultProcessor<SwingMetawidget>() );
      metawidget.putClientProperty( "view-group", "summary" );
      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 {

      @UiViewGroup( { "summary", "detail" } )
      public String name;

      @UiComesAfter( "name" )
      @UiViewGroup( "summary" )
      public int age;

      @UiComesAfter( "name" )
      @UiViewGroup( "detail" )
      public boolean retired;

      @UiComesAfter
      @UiLarge
      public String notes;
   }

   @Retention( RetentionPolicy.RUNTIME )
   @Target( { ElementType.FIELD, ElementType.METHOD } )
   static @interface UiViewGroup {

      String[] value();
   }


   static class ViewGroupInspector
      extends BaseObjectInspector {

      @Override
      protected Map<String, String> inspectTrait( Trait trait )
         throws Exception {

         Map<String, String> attributes = CollectionUtils.newHashMap();
         UiViewGroup viewGroup = trait.getAnnotation( UiViewGroup.class );

         if ( viewGroup != null ) {
            attributes.put( "view-group", ArrayUtils.toString( viewGroup.value() ) );
         }

         return attributes;
      }
   }

   static class ViewGroupInspectionResultProcessor
      implements InspectionResultProcessor<SwingMetawidget> {

      public String processInspectionResult( String inspectionResult, SwingMetawidget metawidget, Object toInspect, String type, String... names ) {

         String viewGroup = (String) metawidget.getClientProperty( "view-group" );
         Document document = XmlUtils.documentFromString( inspectionResult );
         Element entity = (Element) document.getDocumentElement().getFirstChild();

         for ( int loop = 0; loop < entity.getChildNodes().getLength(); ) {

            Element trait = (Element) entity.getChildNodes().item( loop );

            if ( !trait.hasAttribute( "view-group" ))
            {
               loop++;
               continue;
            }

            String[] viewGroups = ArrayUtils.fromString( trait.getAttribute( "view-group" ));

            if ( ArrayUtils.contains( viewGroups, viewGroup )) {
               loop++;
               continue;
            }

            entity.removeChild( trait );
         }

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

3 comments:

Daniel Yokomizo said...

Instead of relying on strings or interfaces (as in JSR-303) wouldn't be better to have meta annotations?

@UiViewGroup
@interface Summary {}

@UiViewGroup
@interface Detail {}


@Summary @Detail
public String name;
@Summary
public int age;
@Detail
public boolean retired;

Richard said...

Daniel,

Great suggestion!

I imagine you could implement it with just a minor change to the ViewGroupInspector in the example above. If you'd like to blog about it, I'd love to link to it.

I continue to be surprised how many alternative approaches there are in this space.

Regards,

Richard.

Dan said...

I was just watching the presentation on guice (on the home page at http://code.google.com/p/google-guice/) on how they handle named injected dependencies of a basic type such as String ... basically its same approach.

Great minds...

-- Dan (H)