1 Reply Latest reply on Feb 8, 2007 12:22 PM by gonzalad

    Navigation patterns

    gonzalad

      Hi,

      I would like to begin a thread giving some samples of navigation patterns (like in chapter 1 of reference guide).

      Since I'm a newbie (just 4 days looking at seam), my first sample can be a bit awkward.

      If someone already started this kind of thread in this forum or elsewhere, please accept my apologies and just post me back a link towards it.
      Also, if you think this post sucks, just tell me.

      The first sample will be for a CRUD navigation and is mostly taken from the booking sample demo.

      The CRUD consists of a list page and a detail page.

      Here's the CRUD navigation specification :
      1. list page allows pagination (next/previous).
      2. list page allows deletion of multiple records.
      3. list page allows selection criteria.
      4. list page allows select detail page and navigate to detail page.
      5. list page allows link to create record.
      6. first acces to list page must be GET.
      7. criteria must be kept somewhere in order to be maintained when navigating from detail to list page.
      8. pagination next/previous or 'a la' Google.
      9. selection criteria must be kept when navigating from detail -> previous.
      10. detail page allows creation / update / delete / cancel, and returns to list view.

      - First solution : use Page scope -

      List and detail data is maintained on page scope.
      This usage appears to be good whenever the detail data can be updated in one screen. Otherwise, you should use Conversation scope for detail data in order to update the detail data on multiple screens.

      I tested this solution with debug.seam searching for a solution which minimizes server or client memory usage.

      This sample uses :
      . employeListAction as action java class for list.
      . emlployeCriteriaList as java class to keep detail records.
      . employeAction as action java class for detail.
      . employeSearch.jspx for list page.
      . employe.jspx for detail page.

      Problems with this solution :

      session scope for keeping criteria data between detail and list navigation.
      using AJAX for client side validation which is quite overkill (I would prefer a pure - and good - javascript solution.


      Here's the code :

      pages.xml

      <page view-id="/employeSearch.jspx">
       <navigation from-action="#{employeListAction.detail}">
       <render view-id="/employe.jspx"/>
       </navigation>
       <navigation from-action="#{employeAction.newEmploye}">
       <render view-id="/employe.jspx"/>
       </navigation>
       </page>
      
       <page view-id="/employe.jspx">
       <!-- pattern redirect after post -->
       <navigation from-action="#{employeAction.saveOrUpdate}">
       <redirect view-id="/employeSearch.jspx"/>
       </navigation>
       <navigation from-action="#{employeAction.delete}">
       <redirect view-id="/employeSearch.jspx"/>
       </navigation>
       <navigation from-action="#{employeAction.cancel}">
       <render view-id="/employeSearch.jspx"/>
       </navigation>
       </page>
      


      List action bean :

      package org.pag.seam.example.employe;
      
      import java.io.Serializable;
      import java.util.ArrayList;
      import java.util.HashMap;
      import java.util.List;
      import java.util.Map;
      
      import org.hibernate.Session;
      import org.jboss.seam.ScopeType;
      import org.jboss.seam.annotations.Factory;
      import org.jboss.seam.annotations.In;
      import org.jboss.seam.annotations.Logger;
      import org.jboss.seam.annotations.Name;
      import org.jboss.seam.annotations.Out;
      import org.jboss.seam.annotations.Scope;
      import org.jboss.seam.annotations.datamodel.DataModel;
      import org.jboss.seam.annotations.datamodel.DataModelSelection;
      import org.jboss.seam.core.FacesMessages;
      import org.jboss.seam.log.Log;
      
      
      @Name("employeListAction")
      @Scope(ScopeType.PAGE)
      public class EmployeListAction implements Serializable {
       private static final long serialVersionUID = -7659479980987467214L;
      
       /**
       * <p>The @DataModelSelection annotation tells Seam to inject
       * the xxx element that corresponded to the clicked link.</p>
       *
       * <p>The @Out annotation then exposes the selected
       * value directly to the page. So ever time a row
       * of the clickable list is selected,
       * the xxx is injected to the attribute of the bean,
       * and the subsequently outjected to the event context
       * variable named message.
       */
       @DataModelSelection
       @In(required=false)
       @Out(required=false)
       private Employe employe;
      
       /**
       * The @DataModel annotation exposes an attibute
       * of type java.util.List to the JSF page as an instance
       * of javax.faces.model.DataModel. This allows us to use
       * the list in a JSF <h:dataTable> with clickable links
       * for each row. In this case, the DataModel is made available
       * in a session context variable named employeList.
       */
       @DataModel
       private List<Employe> employeList;
      
       @In
       private Session bookingDatabase;
      
       @In(create=true)
       @Out(scope=ScopeType.SESSION) // TODO : awkward
       private EmployeListCriteria employeListCriteria;
      
       @Out(scope=ScopeType.PAGE)
       private HashMap<Employe, Boolean> employesToDelete = new HashMap<Employe,Boolean> ();
      
       @Logger
       private Log log;
      
       @In(create=true)
       private FacesMessages facesMessages;
      
       /////////////////////////////////////
       // Propriétés //
       /////////////////////////////////////
       public EmployeListCriteria getEmployeListCriteria() {
       return employeListCriteria;
       }
      
       public void setEmployeListCriteria(EmployeListCriteria employeListCriteria) {
       this.employeListCriteria = employeListCriteria;
       }
      
       /////////////////////////////////////
       // Evènements //
       /////////////////////////////////////
       /**
       * <p>List data is loaded on first access to page .</p>
       */
       @Factory("employeList")
       public void find () {
       getEmployeListCriteria().setPage(0);
       log.info ("find :- #0",getEmployeListCriteria());
       refresh();
       }
      
       public void delete () {
       log.info ("delete ",employesToDelete);
       List<Employe> lEmployes = selectEmployesToDelete();
       if (lEmployes.size() ==0) {
       log.info ("delete lEmployes=", lEmployes);
       facesMessages.add ("No employe selected.");
       return;
       }
       for (Employe lEmploye: lEmployes) {
       delete (lEmploye);
       }
      
       //raffraîchissement des données liste
       refresh();
       }
      
       public void detail () {
       log.info ("detail #0",employe);
       }
      
       public void nextPage()
       {
       if (! isNextPageAvailable()) {
       facesMessages.add ("No more pages available.");
       return;
       }
       getEmployeListCriteria().incPage();
       refresh();
       }
      
       public void previousPage()
       {
       if (! isPreviousPageAvailable()) {
       facesMessages.add ("You already are iun the first page - cakos !");
       return;
       }
       getEmployeListCriteria().decPage();
       refresh();
       }
      
       public boolean isNextPageAvailable()
       {
       return employeList!=null && employeList.size()==getEmployeListCriteria().getPageSize();
       }
      
       public boolean isPreviousPageAvailable()
       {
       return getEmployeListCriteria().getPage()>0;
       }
      
       /////////////////////////////////////
       // Méthodes utilitaires //
       /////////////////////////////////////
       private List<Employe> selectEmployesToDelete () {
       List<Employe> lEmployes = new ArrayList<Employe>();
       if (employeList == null || employesToDelete == null) {
       return lEmployes;
       }
       for (Employe lEmploye: employeList) {
       Boolean selected = employesToDelete.get(lEmploye);
       if (selected!=null && selected) {
       lEmployes.add (lEmploye);
       }
       }
       return lEmployes;
       }
      
       private void delete(Employe employe) {
       log.info ("delete : #0", employe);
       bookingDatabase.delete(employe);
       }
      
       private void refresh() {
       String lSearchString = getEmployeListCriteria().getSearchString();
       String findPattern = lSearchString==null ? "%" : '%' + lSearchString.toLowerCase().replace('*', '%') + '%';
       employeList = (List<Employe>) bookingDatabase.createQuery("select e from Employe e where lower(e.name) like :find")
       .setParameter("find", findPattern)
       .setMaxResults(getEmployeListCriteria().getPageSize())
       .setFirstResult( getEmployeListCriteria().getPage() * getEmployeListCriteria().getPageSize() )
       .list();
       }
      


      Criteria javabean
      package org.pag.seam.example.employe;
      
      import java.io.Serializable;
      
      import org.jboss.seam.ScopeType;
      import org.jboss.seam.annotations.Name;
      import org.jboss.seam.annotations.Scope;
      
      @Name("employeListCriteria")
      @Scope(ScopeType.PAGE)
      public class EmployeListCriteria implements Serializable {
       private static final long serialVersionUID = -3381490105913482464L;
      
       public static final int DEFAULT_PAGE_SIZE = 10;
      
       /**
       * <p>Page devant être affichée.</p>
       *
       * <p>Commence à 0.</p>
       */
       private int page;
       /**
       * <p>Nombre d'enregistrements par page.</p>
       */
       private int pageSize = DEFAULT_PAGE_SIZE;
       /**
       * <p>Critères de filtrage (sélection d'enregistrements).</p>
       */
       private String searchString;
      
       public int getPage() {
       return page;
       }
       public void setPage(int page) {
       this.page = page;
       }
       public int getPageSize() {
       return pageSize;
       }
       public void setPageSize(int pageSize) {
       this.pageSize = pageSize;
       }
       public String getSearchString() {
       return searchString;
       }
       public void setSearchString(String searchString) {
       this.searchString = searchString;
       }
       public void incPage () {
       page++;
       }
       public void decPage () {
       page--;
       }
      
       @Override
       public String toString() {
       return super.toString()+"{searchString="+searchString+"}";
       }
      }
      


      Detail action scope :

      package org.pag.seam.example.employe;
      
      import java.io.Serializable;
      
      import org.hibernate.Session;
      import org.jboss.seam.ScopeType;
      import org.jboss.seam.annotations.Begin;
      import org.jboss.seam.annotations.End;
      import org.jboss.seam.annotations.In;
      import org.jboss.seam.annotations.Logger;
      import org.jboss.seam.annotations.Name;
      import org.jboss.seam.annotations.Out;
      import org.jboss.seam.annotations.Scope;
      import org.jboss.seam.core.FacesMessages;
      import org.jboss.seam.log.Log;
      
      @Name("employeAction")
      @Scope(ScopeType.PAGE)
      public class EmployeAction implements Serializable {
      
       @In
       private Session bookingDatabase;
      
       @In(required=false)
       @Out
       private Employe employe;
      
       private boolean isCreation = false;
      
       @In
       private FacesMessages facesMessages;
      
       @Logger
       private Log log;
      
       /////////////////////////////////////
       // Propriétés //
       /////////////////////////////////////
       public boolean isCreation() {
       return isCreation;
       }
      
       public void setCreation(boolean isCreation) {
       this.isCreation = isCreation;
       }
      
       public void newEmploye() {
       log.debug("newEmploye : ");
       isCreation = true;
       employe = new Employe();
       }
      
       public void delete () {
       bookingDatabase.delete(employe);
       facesMessages.add("Employe #{employe.name} deleted");
       }
      
       public void create () {
       bookingDatabase.save(employe);
       facesMessages.add("Employe #{employe.name} saved");
       }
      
       public void saveOrUpdate () {
       bookingDatabase.saveOrUpdate(employe);
       facesMessages.add("Employe #{employe.name} updated");
       }
      
       public void cancel () {
       }
      
       /////////////////////////////////////
       // Méthodes utilitaires //
       /////////////////////////////////////
      }
      


      list page :
      <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
      <jsp:root xmlns="http://www.w3.org/1999/xhtml"
       xmlns:jsp="http://java.sun.com/JSP/Page"
       xmlns:c="http://java.sun.com/jstl/core"
       xmlns:ui="http://java.sun.com/jsf/facelets"
       xmlns:h="http://java.sun.com/jsf/html"
       xmlns:f="http://java.sun.com/jsf/core"
       xmlns:s="http://jboss.com/products/seam/taglib"
       xmlns:a="https://ajax4jsf.dev.java.net/ajax"
       version="2.0">
      <ui:composition template="template.jspx">
      
      <!-- content -->
      <ui:define name="content">
      <div class="section">
       <h:form id="main">
      
       <span class="errors">
       <a:outputPanel ajaxRendered="true">
       <h:messages/>
       </a:outputPanel>
       </span>
      
       <h1>Search Employees</h1>
       <fieldset>
       <h:inputText id="searchString" value="#{employeListCriteria.searchString}" style="width: 165px;">
       <a:support event="onkeyup" actionListener="#{employeListAction.find}" reRender="searchResults" requestDelay="1000"/>
       </h:inputText>
      
       <a:commandButton id="findEmployes" value="Find Employes" action="#{employeListAction.find}" reRender="searchResults"/>
      
       <a:status>
       <f:facet name="start">
       <h:graphicImage value="/img/spinner.gif"/>
       </f:facet>
       </a:status>
       <br/>
       <h:outputLabel for="pageSize">Maximum results:</h:outputLabel>
       <h:selectOneMenu value="#{employeListCriteria.pageSize}" id="pageSize">
       <f:selectItem itemLabel="5" itemValue="5"/>
       <f:selectItem itemLabel="10" itemValue="10"/>
       <f:selectItem itemLabel="20" itemValue="20"/>
       </h:selectOneMenu>
       </fieldset>
      
       </h:form>
      </div>
      
      <a:outputPanel id="searchResults">
       <div class="section">
       <h:form>
       <h:commandLink value="Create" action="#{employeAction.newEmploye}" propagation="true" style="float:right"/>
       <h:outputText> </h:outputText>
       <h:commandLink value="Delete" action="#{employeListAction.delete}" propagation="true" style="float:right" rendered="#{employeList != null and employeList.rowCount>0}"/>
       <h:outputText value="No Employes Found" rendered="#{employeList != null and employeList.rowCount==0}"/>
       <h:dataTable id="employeList" value="#{employeList}" var="emp" rendered="#{employeList.rowCount>0}">
       <h:column>
       <f:facet name="header"><h:outputText value="#{msgs.searchResultsAdd}" /></f:facet>
       <h:selectBooleanCheckbox value="#{employesToDelete[emp]}"/>
       </h:column>
       <h:column>
       <f:facet name="header">Name</f:facet>
       #{emp.name}
       </h:column>
       <h:column>
       <f:facet name="header">Subname</f:facet>
       #{emp.subname}
       </h:column>
       <h:column>
       <f:facet name="header">birth</f:facet>
       <h:outputText value="#{emp.birthDate}">
       <s:convertDateTime pattern="MM/dd/yyyy"/>
       </h:outputText>
       </h:column>
       <h:column>
       <f:facet name="header">version</f:facet>
       #{emp.version}
       </h:column>
       <h:column>
       <f:facet name="header">Action</f:facet>
       <h:commandLink value="View Employe" action="#{employeListAction.detail}"></h:commandLink>
       <!--s:link id="viewEmploye" value="View Employe" action="#{employeAction.select(emp)}"/-->
       </h:column>
       </h:dataTable>
       <h:commandLink value="Previous results" action="#{employeListAction.previousPage}" rendered="#{employeListAction.previousPageAvailable}" style="float:left"/>
       <!-- h:commandButton value="Delete" action="#{employeListAction.delete}" rendered="#{employeList != null and employeList.rowCount>0}" style="float:left"/-->
       <h:commandLink value="More results" action="#{employeListAction.nextPage}" rendered="#{employeListAction.nextPageAvailable}" style="float:right"/>
       </h:form>
       </div>
      </a:outputPanel>
      
      </ui:define>
      
      
      <!-- sidebar -->
      <ui:define name="sidebar">
      
      <h1>CRUD usage</h1>
      <p>
       XXX
      </p>
      </ui:define>
      </ui:composition>
      </jsp:root>
      


      Detail page :
      <!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
      <jsp:root xmlns="http://www.w3.org/1999/xhtml"
       xmlns:jsp="http://java.sun.com/JSP/Page"
       xmlns:ui="http://java.sun.com/jsf/facelets"
       xmlns:h="http://java.sun.com/jsf/html"
       xmlns:f="http://java.sun.com/jsf/core"
       xmlns:s="http://jboss.com/products/seam/taglib"
       xmlns:a="https://ajax4jsf.dev.java.net/ajax"
       version="2.0">
      <ui:composition template="template.jspx">
      
      <!-- content -->
      <ui:define name="content">
      <div class="section">
       <h1>Employe</h1>
      </div>
      <div class="section">
       <h:form id="booking">
       <fieldset>
       <s:validateAll>
      
       <f:facet name="aroundInvalidField">
       <s:span styleClass="errors"/>
       </f:facet>
       <div class="entry">
       <div class="label"><h:outputLabel for="name">Namel:</h:outputLabel></div>
       <div class="input">
       <s:decorate>
       <h:inputText id="name" value="#{employe.name}" required="true">
       <a:support event="onblur" reRender="nameErrors"/>
       </h:inputText>
       <br/>
       <a:outputPanel id="nameErrors"><s:message/></a:outputPanel>
       </s:decorate>
       </div>
       </div>
       <div class="entry">
       <div class="label"><h:outputLabel for="subname">Subname:</h:outputLabel></div>
       <div class="input">
       <s:decorate>
       <h:inputText id="subname" value="#{employe.subname}" required="true">
       <a:support event="onblur" reRender="subnameErrors"/>
       </h:inputText>
       <br/>
       <a:outputPanel id="subnameErrors"><s:message/></a:outputPanel>
       </s:decorate>
       </div>
       </div>
       <div class="entry">
       <div class="label"><h:outputLabel for="birthDate">Birthdate:</h:outputLabel></div>
       <div class="input">
       <s:decorate>
       <h:inputText id="birthDate" value="#{employe.birthDate}" required="false">
       <s:convertDateTime pattern="MM/dd/yyyy"/>
       </h:inputText>
       <s:selectDate for="birthDate">
       <h:graphicImage url="img/dtpick.gif" style="margin-left:5px"/>
       </s:selectDate>
       <br/>
       <a:outputPanel id="birthDateErrors"><s:message/></a:outputPanel>
       </s:decorate>
       </div>
       </div>
       </s:validateAll>
      
       <div class="entry errors">
       <h:messages globalOnly="true"/>
       </div>
      
       <div class="entry">
       <div class="label"> </div>
       <div class="input">
       <h:commandButton id="update" value="Update" action="#{employeAction.saveOrUpdate}" rendered="#{not employeAction.creation}"/>
       <h:commandButton id="save" value="Save" action="#{employeAction.saveOrUpdate}" rendered="#{employeAction.creation}"/>
       <h:commandButton id="delete" value="Delete" action="#{employeAction.delete}" rendered="#{not employeAction.creation}"/>
       <h:commandButton id="cancel" value="Cancel" action="#{employeAction.cancel}" immediate="true"/>
       </div>
       </div>
       </fieldset>
       </h:form>
      </div>
      </ui:define>
      
      <!-- sidebar -->
      <ui:define name="sidebar">
      
      <h1>TODO</h1>
      <p>
       Complete page description.
      </p>
      <p>
       <a href="#" onclick="window.open('exp/workspaceExp.html','exp','width=752,height=500,scrollbars=yes');">
       How does the workspace list work?
       </a>
      </p>
      
      </ui:define>
      
      </ui:composition>
      </jsp:root>
      


      Entity class :
      package org.pag.seam.example.employe;
      
      import java.io.Serializable;
      import java.util.Date;
      
      import javax.persistence.Basic;
      import javax.persistence.Entity;
      import javax.persistence.GeneratedValue;
      import javax.persistence.Id;
      import javax.persistence.Temporal;
      import javax.persistence.TemporalType;
      import javax.persistence.Version;
      
      import org.hibernate.validator.Length;
      import org.hibernate.validator.NotNull;
      import org.jboss.seam.ScopeType;
      import org.jboss.seam.annotations.Name;
      import org.jboss.seam.annotations.Scope;
      
      @Entity
      @Name("employe")
      @Scope(ScopeType.PAGE)
      public class Employe implements Serializable {
      
       private static final long serialVersionUID = -3086982759705351765L;
      
       @Id @GeneratedValue
       private Long id = null;
       @Version
       private int version = 0;
       @Length(max=30) @NotNull
       private String name = null;
       @Length(max=30) @NotNull
       private String subname = null;
       @Basic @Temporal(TemporalType.DATE)
       private Date birthDate;
      
       public Date getBirthDate() {
       return birthDate;
       }
       public void setBirthDate(Date aBirthDate) {
       birthDate = aBirthDate;
       }
      
       public Long getId() {
       return id;
       }
       public void setId(Long aId) {
       id = aId;
       }
      
       public String getName() {
       return name;
       }
       public void setName(String aName) {
       name = aName;
       }
      
       public String getSubname() {
       return subname;
       }
       public void setSubname(String aSubname) {
       subname = aSubname;
       }
       public int getVersion() {
       return version;
       }
       public void setVersion(int aVersion) {
       version = aVersion;
       }
      
      
      }
      



        • 1. Re: Navigation patterns
          gonzalad

          Another downside of page scope.

          For HTML page size :
          list page = 81 ko.

          There are 2 forms on this page (in to AJAX criteria entry and to render list data on key press).
          First form (criteria data) is 24ko/
          Second one (list data for 10 rows) is 48 ko.