Tuesday, November 16, 2010

Duplication in User Interfaces: 6 Interviews With Industry Practitioners

Abridged from Kennard, R., Edmonds, E. & Leaney, J. 2009, 2nd International Conference on Human System Interaction

1. Introduction

A peer reviewer of our earlier paper on UI generation (Kennard & Steele 2008) questioned whether 'UI duplication' (that is, restating information between the UI and the business layer) was really a problem. Was it really a detrimental source of errors, or did it provide an advantageous level of flexibility? At the time I had to concede I lacked evidence other than my own experience as an industry practitioner. Given this was a fundamental premise of my thesis on user interface generation, it was important to validate it. To do so, I conducted 6 interviews with senior software developers chosen from different segments of industry - including finance, medical and middleware - across the UK, the US and Australia.

I chose a standardized, open-ended format for the interviews (Valenzuela & Shrivastava 2002). This approach involves asking the same standardized set of questions to each interviewee, but the set is necessarily short because each question is framed broadly so as to allow the candidates to talk openly about their experiences. Standardized, open-ended interviews allow accurate comparison and analysis of results, whilst avoiding leading the interview and therefore minimizing bias. To analyse the results, I employed a simplified version of Grounded Theory (Dick 2005). This approach involves coding, comparing and sorting categories that emerge from the interview sessions. Of principal interest to this paper was the category of duplication. Other categories that emerged will be used in future work.

2. Duplication

I began each interview by informing the practitioner I wanted to talk about the mechanics, not the aesthetics, of developing a UI and its relation to the rest of an application. I asked each practitioner to describe the process they would go through to add, say, a Date of Birth field to an existing Person business object in their current software system, including both the back end and front end. This initial question was deliberately phrased to be as open-ended as possible. Specifically, it avoided the bias of mentioning duplication. However, because I didn't explicitly prompt duplication, it was important to have each practitioner talk not just about the UI but all steps of the process, from back end to front end. In this way, the duplication would become apparent of its own accord. Finally, I phrased the question around updating an existing business object, rather than a new one. I did this in order to expose the weaknesses of static code generation tools, but again because I didn't explicitly refer to 'static code generation' I didn't feel this biased the responses.

All the practitioners gave answers similar to the example in the previous section. One enumerated "first off we would add [the Date of Birth field] to the database, in the table. We'd then add it to the stored procedures going up. Add it into the Data Access Layer for the purposes of getting it out of the recordset. And then you'd add the property into the business level, the business layer. And then, on the UI, on the front-end, we'd have to add the field in the HTML". Another practitioner said "I would go to the persistence level, I'd work out how that field should be modelled in the problem domain. For date of birth, you'd have a date column. I'd look at the Person class, work out its relationship with the Person schema. Work out its name, what its type would be, date or datetime depending on the database. Then I'd work out how I should change the Person class - they'd probably just be a getter and setter - and then I'd tie it back to the persistence layer, map it back to the table. For validation constraints, yeah, this is always a problem, you need to validate it both in the UI and at the persistence layer if that's a business rule, so it's always a problem. In terms of the UI, I'd go and find the bit of UI code and work out the position where this field should be added".

It was noted those practitioners using newer technologies had considerably fewer steps. One said "we would obviously add that field to the actual business object that [JPA] maps to the database, that's already there. And then any validation constraints that are around that - we use Hibernate Validator so we'd put the validation constraints on the entity, we don't have to do anything more for validation other than that, and all that's left now is dropping the field on to the UI, and that should be it really. Using the IDE we have we'd drag and drop UI components, then we'd have to apply some kind of formatting as well, some formatting to the underlying XHTML". However I observed this sub-category (Dick 2005) of reducing steps was generally from the business objects 'down' through to the persistence layer, removing the manual coding of schemas, stored procedures and recordsets, not 'up' to the UI layer.

I then summarised the steps back to the practitioner and asked whether they thought any steps were deficient. Not all the practitioners were immediately aware of any problem. This is to be expected for such an entrenched issue: some interviewees simply don't know any different. One said "what we have now is pretty good, certainly compared to a Java Server Pages (JSP) or something like that. 2 steps to add a field is pretty good. The framework handles quite a lot and we can develop much faster than we normally do". For those practitioners I used a further probe question (Valenzuela & Shrivastava 2002), which specifically raised awareness of restating information. I asked whether any steps seemed redundant, or contained duplicated information from previous steps. Such a question has inherent bias, so it was not asked unless the practitioner failed to identify duplication naturally.

Following the probe question, all interviewees converged on recognising duplication amongst the steps. "The problem definitely exists. It's more from the business layer forward to the screen is the biggest problem because there are things out there like Hibernate (Hibernate 2009) which do from, sort of, business layer down". Another echoed this sentiment "the drudgery at the moment is adding the UI code, and adding the validation and giving that feedback. That's really quite unpleasant. It's the most complex of all the steps, actually, depending on the magnitude of the change. Given a very simple change, just adding a single field, the bulk of the work, the bulk of the drudgery, in the coding is at the UI level. Being able to more concisely express the relationship between the UI and the model and the change I want to make in one place, or at most two places, in a very concise fashion would help". Another warned "it's a fairly established software engineering principle that the more you have to repeat something the higher the error is, the higher the chances there's going to be an error in the code".

3. Defects

Following on from this, I asked each practitioner whether they had ever encountered defects that were a result of this recognised deficiency in their process. All of the interviewees responded that such defects were common. "Definitely. There's always a chance that someone's going to get a bug somewhere along the line, especially with Date of Birth - as it goes down the date gets mixed up because someone's used the incorrect data type. With some of our junior developers we have here that's quite a common thing where they get a bit muddled up... it's definitely an issue that should be far simpler". Another agreed "All the time. That would be me overlooking various aspects of the user feedback loop, in the validation, me forgetting to persist various fields that I've added, so the validation happens but then it never persisted, so having to tie the new field to the model, with validation, in multiple places, gives a number of points where I could fail to do that". Another said, of reviewing other developer's code, "a large percentage of mistakes were always they'd copy and pasted and they'd changed that one, and that one, and that one - but not that one. So it creates a higher chance of there being a minor error".

Several practitioners echoed this difficulty of identifying duplication related defects, because they generally evade static checking and developers must rely on runtime testing to detect them. One financial software practitioner explained "we've got a BigDecimal (Gosling 2005), and [the back end has] set the scale to 8 but the UI puts through 10, it [gets silently rounded and] passes all the way through. That becomes a real issue because it's really hard to find. That's caused us huge problems before". Another agreed "it's the biggest problem I personally face. These sorts of errors. You're updating, say, you change the type of a field and you try updating it with, say, a datetime object but you've actually now changed it to an integer field, you don't realise until you actually start testing the application, or if you miss it in testing and send it out to customers, you don't realise that there's a problem until you get the bug reports - not ideal".

One practitioner described how, because duplication is generally not understood by refactoring tools, it works against his preferred methodology of aggressive refactoring: "if you change a field name, and I do like to change field names - I don't know why - so I'll decide after a year of using the program 'what's that field name doing there?' I did it the other day: I've got a stock control module in the program and there's [a field] called stock_reorder_level_reminder and I thought 'what a stupid name for a field', so I just changed it to reorder_level because that's much easier. Now, generally changing that could have massive implications couldn't it? You could change that and it could break the application in several parts".

4. Prevalence

Finally, I asked each practitioner whether the themes explored in the interview were commonplace across all software systems they had developed. One said "I've built a number of UIs over the course of my career, some of them have been desktop applications, some of them have been web applications, and I think this is a general problem. For desktop applications it's hard but it's relatively easy. For web I think it becomes a lot more difficult because the technologies involved are a lot more fiddly, there are a lot more moving parts in web application UIs. But yes I think it's a general problem.". Another said "quite honestly laying out UI forms is time consuming, it's fairly standard how a UI is - it shouldn't be a problem to say, okay, you have these things you probably want to interface in a particular way, here's what we suggest - we being the computer - you've got a datetime here, here's the calendar control we suggest. Oh you don't want a calendar, you want to use a textbox, go for it. Something along those lines would definitely detract from the tedium of putting together the UI, which is an important step and everything but is a really repetitive process. If it's a varchar in the database, it's going to manifest as some form of a textbox on the form. If I've got a foreign key in my database, it's going to manifest as some form of listbox, dropdown, radio button, checkbox. It's not a huge leap". One practitioner summarised it as "every developer who writes anything more than a Hello World application will have this problem. Most developers who strive to make their work better, who aren't lazy, do sense this problem, do encounter this problem on a daily basis as a constant friction in their daily work".

I observed a sub-category (Dick 2005) that this friction had driven several practitioners to fashion their own ad hoc solutions by combining existing tools. "For a brand new screen we're currently using CodeSmith (CodeSmith 2009), so if you design the database table you can hit generate and it'll go through and generate everything right up to the screen". However, because of subsequent editing of the generated code, they found CodeSmith to be of limited use outside of new screens: "if you could do the same thing where you could add a new field to the database and it generated and added it into the [existing] code for you as it goes up that'd be excellent". Other solutions had similar shortcomings. Microsoft LinQ (LinQ 2009) helped with the persistence layer, but "if I go in and create a field, LinQ creates a nullable version of that field, where the [UI] control I'm binding it to is expecting a non-nullable version. That's caused a number of problems. That's come up a number of times and you've really got to kind of juggle to make it work right. Keep in mind when that could happen and keep track of the potential for it to happen". Asked why they had invested the considerable resources to fashion their own solutions: "I do genuinely believe that kind of thing makes the development cycle better in the long run. It makes things much cleaner, there's less coding to go on. If I were to have to write, well, in my application the basic objects I have, I have patients, contacts, appointments, items, invoice, payments, refunds, credits and then a load of secondary objects like appointment status', patient categories, all of these are objects. If I had to code a separate form for each one it's just tedious. Interface work is not that much fun. It's quite tedious, dropping controls on a form, lining them up with the other controls and fiddling around for ages". Another practitioner echoed this sentiment saying, if such tedium could be reduced, "you'd have more time for the actual problem solving: defining, clarifying, implementing the problem rather than the mechanics of the 'auto pilot' of gotta code up this method, gotta code this, gotta code that. Give you more time to concentrate on the more energy-requiring things rather than the monotonous reproducing of stuff. Because, I mean, despite the fact they tell everyone not to, normally you end up copying and pasting things".

5. Conclusion

The results of my interviews suggested UI duplication was indeed a prevalent and serious problem in software development. I observed developers across industry segments and across software platforms, and saw they had common experiences of duplication, common experiences of bugs caused by it, and a common desire for it to be addressed. These results were compiled into a research paper (Kennard, Edmonds & Leaney 2009) and this time its reviewers seemed more convinced. One wrote "In this paper, the authors provide extensive evidence for the problem of manual code duplication among subsystems in the process of software... the work is exceptionally well motivated". Another: "the work is important for the [research] community to hear... nobody in a senior position in a software company today is going to not be aware of this problem and the various hacks, workarounds, and band-aids used to cover up the pain that it causes (at least not in a successful software company). Again, academics may be surprised by some of the comments and the gravity of the concerns, but I wasn't; this is a serious problem and there are partial solutions out there that various people employ with varying degrees of success".

References

CodeSmith. 2009. http://codesmithtools.com
Dick, B. 2005, 'Grounded theory: a thumbnail sketch', http://www.scu.edu.au/schools/gcm/ar/arp/grounded.html
Gosling, J. 2005, The Java Language Specification, Addison-Wesley.
Hibernate. 2008. http://www.hibernate.org
Kennard, R., Edmonds, E. & Leaney, J. 2009, Separation Anxiety: stresses of developing a modern day Separable User Interface. 2nd International Conference on Human System Interaction.
Kennard, R. & Steele, R. 2008, Application of Software Mining to Automatic User Interface Generation. 7th International Conference on Software Methodologies, Tools and Techniques.
LinQ 2009. http://msdn.microsoft.com/en-us/vcsharp/aa904594.aspx
Valenzuela, D. & Shrivastava, P. 2002, 'Interview as a Method for Qualitative Research', http://www.public.asu.edu/~kroel/www500/Interview%20Fri.pdf.

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 1: looks like this approach may be making it into the JSF spec: http://java.net/jira/browse/JAVASERVERFACES_SPEC_PUBLIC-1007

UPDATE 2: for AJAX requests, you also need to test partialViewContext.isAjaxRequest (see comments below)

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 );
      }
   }
}