3 Replies Latest reply on Oct 28, 2008 1:48 AM by Tim Otten

    RFC: CRUD with model-driven facelets

    Tim Otten Newbie

      I am new to Seam and JSF, and I'm working on a project that intends to create, tweak, and maintain a number of Seam-based CRUD UI's. For this project, I've been experimenting with a model-driven technique for constructing JSF views.  The technique significantly reduces boilerplate and improves maintainablity for some normal cases, and it doesn't require much Seam/JSF knowledge, but it does have some issues. I'd like to get some third-party perspective on this technique: Does it seem good? Should I run away from it? Would other people benefit from it?


      First, a little glib CW: Code generators, like seam-gen, can help developers get going quickly, and they provide many spots where developers can replace the output with hand-crafted code. Unfortunately, code generators also produce large volumes of slightly-different code, and maintaining that code can suck. In particular, how do we handle systematic changes to code? Perhaps we re-run the generator (and lose our customizations), or we perhaps we get familiar with copy/paste.


      I've had spotted a few cases in my project where these limitations were apparent -- cases where I wanted to



      • Implement a click-to-edit UI that applies to all inputs in handful of views.

      • Add little widgets depending on a property's type or annotations (e.g. whenever we display or edit an @Email property, we should show a little mailto link)

      • Hook-up converters and validators depending on a property's type or annotations. (e.g. whenever we edit a @PhoneNumber property, hook up a phoneNumberConverter)

      • Change the type-information for a property.



      In all of these cases, I was tempted to copy-paste like a maniac, to edit the seam-gen templates, and/or to re-run seam-gen. I settled on an approach which simplifies the seam-gen templates, removes some logic from seam-gen, and handles the logic at runtime.


      The core idea: Suppose we have a CRUD page that needs to display a property of a Java bean. A small component (PropertyTemplateManager) searches for a template file that specifies how to display the property.


      To see how this idea plays out, I describe some key code snippets in an example app. I don't have a sample app that I can share right now (for non-technical reasons), but I'd be happy to build one.


      (Aside: The original JavaBeans specification includes a mechanism for model-driven view construction. This seems similar. To my understanding, though, the JavaBeans spec is geared toward AWT/Swing applications. This mechanism is designed for JSF/Seam applications.)



      Example: The Model




      @Entity
      public class Contact ... {
          @Email
          @Length(max=80)
          private contactEmail;
        
          @PhoneNumber
          @Length(max=10)
          private String cellPhone;
      
          ...
      }
      




      Example: An Edit page with normal seam-gen





      Given the above model, seam-gen would produce an edit page that looks a bit like this:


      <!-- File: /ContactEdit.xhtml -->
      ...
              <rich:panel>
                  <f:facet name="header">#{contactHome}.managed ? 'Edit' : 'Add'} Seminar</f:facet>
      
                  <s:decorate id="emailDecoration" template="layout/edit.xhtml">
                      <ui:define name="label">email</ui:define>
                      <h:inputText id="email"
                                 size="60"
                            maxlength="80"
                                value="#{contactHome}.instance.email}">
                          <a:support event="onblur" reRender="emailDecoration" bypassUpdates="true" ajaxSingle="true"/>
                      </h:inputText>
                  </s:decorate>
      
                  <s:decorate id="cellPhoneDecoration" template="layout/edit.xhtml">
                      <ui:define name="label">cellPhone</ui:define>
                      <h:inputText id="cellPhone"
                                 size="60"
                            maxlength="80"
                                value="#{contactHome}.instance.cellPhone}">
                          <a:support event="onblur" reRender="cellPhoneDecoration" bypassUpdates="true" ajaxSingle="true"/>
                      </h:inputText>
                  </s:decorate>
      
              </rich:panel>
      ...
      




      Example: An Edit page with property-templates




      With property-templates, the code-generator doesn't make as many decisions about the markup produced for each property. Instead, that decision is delegated to the <dui:include> tag.


      <!-- File: /SeminarEdit.xhtml -->
      ...
              <rich:panel>
                  <f:facet name="header">#{contactHome}.managed ? 'Edit' : 'Add'} Contact</f:facet>
      
                  <dui:bean
                      beanClass="org.example.entity.Contact"
                      bean="#{contactHome.instance}"
                      viewType="edit">
      
                    <dui:include 
                        id="emailDecoration"
                        property="email" />
      
                    <dui:include
                        id="cellPhoneDecoration"
                        property="cellPhone" />
      
                  </dui:bean>
      
              </rich:panel>
      ...
      




      Example: Locating property templates




      The <dui:include> tags are similar to <ui:include>, but the dui version is more dynamic -- it triggers a search (using the PropertyTemplateManager) to dynamically select a template file. For example, given property=email, the search will select the first available file from this list:



      1. /WEB-INF/property/com/example/annotations/Email-edit.xhtml

      2. /WEB-INF/property/org/hibernate/validator/Length-edit.xhtml

      3. /WEB-INF/property/java/lang/String-edit.xhtml

      4. /WEB-INF/property/java/lang/Object-edit.xhtml

      5. /WEB-INF/property/default.xhtml



      The search for cellPhone will choose the first available file from this list:



      1. /WEB-INF/property/com/example/annotations/PhoneNumber-edit.xhtml

      2. /WEB-INF/property/org/hibernate/validator/Length-edit.xhtml

      3. /WEB-INF/property/java/lang/String-edit.xhtml

      4. /WEB-INF/property/java/lang/Object-edit.xhtml

      5. /WEB-INF/property/default.xhtml




      Example: Designing a property template




      The first property-template will be a generic one that works for almost any property of any simple type (String, Boolean, double, etc). It relies on JSF/Seam to provide an appropriate converter:


      <!-- File: /WEB-INF/property/java/lang/Object-edit.xhtml -->
      <ui:composition>
              <s:decorate id="#{id}" template="/layout/edit.xhtml">
                      <ui:define name="label">#{messages[property]}</ui:define>
                      <h:inputText
                              value="#{bean[property]}"
                              required="#{requiredProperty == null ? false : requiredProperty}"
                              size="#{(size != null) ? size : 32}"
                              maxlength="#{(maxlength != null) ? maxlength : (size != null ? size : 32) }">
                              <a:support event="onblur" reRender="#{id}" bypassUpdates="true" ajaxSingle="true"/>
                      </h:inputText>
              </s:decorate>
      </ui:composition>
      



      This template resembles the seam-gen code, but it plugs in the parameters from <dui:bean> and <dui:include>. In particular, notice expressions like <s:decorate id=#{id}> and <h:inputText value=#{bean[property]}>.


      Of course, like the seam-gen code, this template is pretty generic -- it's designed to work with almost any property. It doesn't provide a very rich interface. We should provide specialized templates based on the type of information we're trying to edit. In the following example, we define a richer UI for @Email properties. The UI includes a mailto link.



      <!-- File: /WEB-INF/property/com/example/annotations/Email-edit.xhtml -->
      <ui:composition>
              <s:decorate id="#{id}" template="/layout/edit.xhtml" styleClass="emailProperty">
                      <ui:define name="label">#{messages[property]}</ui:define>
                      <h:inputText
                              value="#{bean[property]}"
                              styleClass="emailPropertyInput"
                              required="#{requiredProperty == null ? false : requiredProperty}"
                              size="#{(size != null) ? size : 32}"
                              maxlength="#{(maxlength != null) ? maxlength : (size != null ? size : 32) }">
                              <a:support event="onblur" reRender="#{id}" bypassUpdates="true" ajaxSingle="true"/>
                      </h:inputText>
      
                      <s:span rendered="${! empty bean[property]}">
                              [<a href="#" onclick="window.location = 'mailto:'+findCousinsByClassName(this,'emailProperty','emailPropertyInput')[0].value);return false;">Open</a>
                      </s:span>
              </s:decorate>
      



      The second template resembles the first: both of them include an <s:decorate template=/layout/edit.xhtml> tag. Both define a label. Both use the id parameter. It is very handy to ensure that all PT's that are designed for viewType=edit have these similar features.


      More generally, for viewType=edit, there is a contract between the broader page (/ContactEdit.xhtml) and the property templates (/WEB-INF/property/foo-edit.xhtml). The broader page assumes that any template will include an <s:decorate> tag.


      We can adapt the <dui:include> technique to other pages (/Contact.xhtml or /ContactList.xhtml) by adding new viewTypes. Of course, different viewTypes use different contracts. With list views, the broader page assumes that the template will include an <h:column> tag instead of an <s:decorate> tag.



      Considerations, Limitations, Issues




      The <dui:include> is useful for many properties, but it is not mandatory. You can easily mix-and-match property templates with customized code.


      Sometimes, the view for one property should be wired to the view for another property.In these cases, you might tweak the contract or rip-out the <dui:include>.


      The <dui:bean> and <dui:include> tag require you to explicitly define beanClass and property. There are some alternative formulations (such as <dui:include property=#{myHome.instance.foo}>) which look nicer but are not possible. There are a few limitations which lead to the current formulation:



      • JSF pages describe a static component graph. When page processing begins, we have to construct the right graph. The graph cannot change as the data changes. Moreover, we must be able to construct the graph even if the data is null.

      • A static examination of a ValueExpression could tell us the property's type, but not always. For example, any EL that references a Map or null can lose type information.

      • ValueExpressions don't provide access to annotations.



      If you create a new .xhtml file, the file may not be used automatically. A Full Publish will fix this. (There may be less drastic ways to force PTM/JSF to recognize the new template, but I need to test to verify.)


      The templates don't currently have access to the annotation data, but this could be very useful:



      • e.g. in /WEB-INF/property/java/lang/Object-edit.xhtml, the <h:inputText maxlength=XYZ> could be harmonized with @Length(max=XYZ).

      • e.g. in /WEB-INF/property/java/util/Date-edit.xhtml, we should exploit the @Temporal annotation.



      There can be re-entrancy issues -- e.g. if a <dui:include> tag includes a template which includes another <dui:include> tag, then parameters for the outer template may leak to the inner template. This is probably solvable, but it hasn't been a problem for me.