Building master-detail views using Seam is a regular topic on the forums, and with good reason. It's not trivial for new developers.
There are three levels that have to work together:
- The Hibernate mapping between the master and detail entities
- The Seam layer - the EntityHome or other bean
- The presentation layer
In terms of requirements for a maintenance screen that deals with both master and detail:
- you want the user to update both master and detail records on the same page
- you want to save changes to master and detail at the end (or optionally part-way through)
- you should be able to add new detail rows or delete existing ones and see the changes reflected immediately on the screen, again without navigating between pages
So to an example. My two entities are Master and Detail, represented in the MySQL database as:
CREATE TABLE master (
id int(11) NOT NULL AUTO_INCREMENT,
master_description varchar(100) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM
CREATE TABLE detail (
id int(11) NOT NULL AUTO_INCREMENT,
master_id int(11) NOT NULL,
detail_description varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY newfk (master_id)
) ENGINE=MyISAM
The Hibernate mappings need to reflect the one-to-many relationship from master to detail.
@Entity
@Table(name = "master", catalog = "test")
public class Master implements java.io.Serializable {
private Integer id;
private String masterDescription;
private List<detail> details = new Vector<detail>(0);
public Master() {
}
public Master(String masterDescription, List<detail>details) {
this.masterDescription = masterDescription;
this.details = details;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@Column(name = "master_description", length = 100)
@Length(max = 100)
public String getMasterDescription() {
return this.masterDescription;
}
public void setMasterDescription(String masterDescription) {
this.masterDescription = masterDescription;
}
@OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY, mappedBy = "master")
public List<detail> getDetails() {
return this.details;
}
public void setDetails(List<detail> details) {
this.details = details;
}
}
Note that this example uses List and Vector instead of the Sets that Seam-gen uses by default. Select lists and some other presentation widgets seem to require Lists rather than Sets, and it's become a habit to use them instead of the default Seam-gen code.
@Entity
@Table(name = "detail", catalog = "test")
public class Detail implements java.io.Serializable {
private Integer id;
private String detailDescription;
private Master master;
public Detail() {
}
public Detail(Master master, String detailDescription) {
this.master = master;
this.detailDescription = detailDescription;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "master_id", nullable = false)
@NotNull
public Master getMaster() {
return this.master;
}
public void setMaster(Master master) {
this.master = master;
}
@Column(name = "detail_description", nullable = false, length = 100)
@NotNull
@Length(max = 100)
public String getDetailDescription() {
return this.detailDescription;
}
public void setDetailDescription(String detailDescription) {
this.detailDescription = detailDescription;
}
}
MappedBy = "master" identifies the name of the property on the detail entity that links back to the master entity.
Now we need a backing bean for the maintenance page. Seam-gen will give you a set of XXXList, XXX, and XXXEdit pages for each entity. The XXX and XXXEdit pages refer to an EntityHome instance for the entity to be viewed/edited. So after running Seam-gen to reverse-engineer from the MySQL database we will have a set of XHTML pages
- Master.xhtml
- MasterEdit.xhtml
- MasterList.xhtml
- Detail.xhtml
- DetailEdit.xhtml
- DetailList.xhtml
and a set of four backing beans:
- MasterHome
- MasterList
- DetailHome
- DetailList
Really we want to end up with a single page, and therefore a single backing bean that encompasses the functions of both MasterHome and DetailHome.
@Name("masterEditor")
public class MasterEditor extends EntityHome<Master> {
@In EntityManager entityManager;
@DataModel private List<Detail> detailListFromMaster = new Vector<Detail>(0);
@DataModelSelection @Out(required=false) private Detail currentlySelectedDetail;
//For the selection on the detail list
public void select() {}
public void createNewDetail() {
Detail detail = new Detail();
detail.setMaster(getInstance());
detailListFromMaster.add(detail);
currentlySelectedDetail = detail;
}
public void cancelNewDetail() {
removeDetail();
}
//Delete a detail item
public void removeDetail() {
detailListFromMaster.remove(currentlySelectedDetail);
entityManager.remove(currentlySelectedDetail);
}
public void setMasterId(Integer id) {
setId(id);
}
public Integer getMasterId() {
return (Integer) getId();
}
@Override
public String update() {
return super.update();
}
@Override
protected Master createInstance() {
Master master = new Master();
return master;
}
public void wire() {
//Both triggers getInstance() and sets the local variable
//to the list of Detail items from the Master entity
detailListFromMaster = getInstance().getDetails();
}
public boolean isWired() {
return true;
}
public Master getDefinedInstance() {
return isIdDefined() ? getInstance() : null;
}
}
This is an edited EntityHome, which started life as MasterHome. A few things to note:
- we want to use a DataModel and DataModelSelection to link with the presentation layer
- the wire() function now gets the list of Detail objects from the Master object and pushes them into the DataModel list
- there are a few simple functions added to be triggered from the presentation layer to do simple CRUD stuff (add a new Detail, remove, etc)
So on to the presentation layer. This is in two parts - the main page, plus a Modal Panel, all using RichFaces.
[UPDATE: Note that if you want a presentation technique that does not use Modal Panels, try
this more recent post.
]
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:a="http://richfaces.org/a4j"
xmlns:rich="http://richfaces.org/rich"
template="layout/template.xhtml">
<ui:define name="body">
<rich:modalPanel id="detailEditWizard"
minHeight="200"
minWidth="500"
autosized="true">
<f:facet name="header"><h:outputText value="Edit Detail..." /></f:facet>
<f:facet name="controls">
<h:graphicImage value="/img/ico_close.gif"
styleClass="linkImage"
onclick="#{rich:component('detailEditWizard')}.hide()" />
</f:facet>
<h:form id="detailEditForm" styleClass="edit">
<h:messages globalOnly="true"/>
<s:decorate id="masterIdField" template="layout/edit.xhtml">
<ui:define name="label">Master id</ui:define>
<h:inputText id="masterId"
required="true"
value="#{currentlySelectedDetail.master.id}">
<a:support event="onblur" reRender="masterIdField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>
<s:decorate id="detailDescriptionField" template="layout/edit.xhtml">
<ui:define name="label">Detail description</ui:define>
<h:inputText id="detailDescription"
required="true"
size="100"
maxlength="100"
value="#{currentlySelectedDetail.detailDescription}">
<a:support event="onblur" reRender="detailDescriptionField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>
<div class="actionButtons" style="clear:both">
<a:commandButton id="detailEditMPClose"
onclick="#{rich:component('detailEditWizard')}.hide()"
reRender="detailListTable"
value="Close" />
<a:commandButton id="detailEditMPCancel"
onclick="#{rich:component('detailEditWizard')}.hide()"
action="#{masterEditor.cancelNewDetail()}"
reRender="detailListTable"
value="Cancel" />
</div>
</h:form>
</rich:modalPanel>
<h:form id="master" styleClass="edit">
<rich:panel>
<f:facet name="header">#{masterEditor.managed ? 'Edit' : 'Add'} Master</f:facet>
<s:decorate id="masterDescriptionField" template="layout/edit.xhtml">
<ui:define name="label">Master description</ui:define>
<h:inputText id="masterDescription"
size="100"
maxlength="100"
value="#{masterEditor.instance.masterDescription}">
<a:support event="onblur" reRender="masterDescriptionField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>
<div style="clear:both">
<span class="required">*</span>
required fields
</div>
</rich:panel>
<!-- Detail section -->
<rich:panel>
<f:facet name="header">Detail List</f:facet>
<div class="results" id="detailList">
<h:outputText value="The detail search returned no results."
rendered="#{empty masterEditor.instance.details}"/>
<rich:dataTable id="detailListTable"
var="_detail"
value="#{detailListFromMaster}"
rendered="#{not empty detailListFromMaster}">
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Id"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.id}"/>
</h:column>
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Master id"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.master.id}"/>
</h:column>
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Detail description"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.detailDescription}"/>
</h:column>
<rich:column styleClass="action">
<f:facet name="header">Action</f:facet>
<a:commandLink id="editDetailMP"
action="#{masterEditor.select()}"
oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
reRender="detailEditForm"
value="Edit">
<s:conversationId />
</a:commandLink>
#{' '}
<a:commandLink id="removeDetail"
action="#{masterEditor.removeDetail()}"
reRender="detailListTable"
value="Delete">
<s:conversationId />
</a:commandLink>
</rich:column>
</rich:dataTable>
</div>
<s:div styleClass="actionButtons" rendered="#{empty from}">
<a:commandLink id="editDetailMP"
action="#{masterEditor.createNewDetail()}"
oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
reRender="detailEditForm"
value="New Detail...">
<s:conversationId />
</a:commandLink>
</s:div>
</rich:panel>
<div class="actionButtons">
<h:commandButton id="save"
value="Save"
action="#{masterEditor.persist}"
disabled="#{!masterEditor.wired}"
rendered="#{!masterEditor.managed}"/>
<h:commandButton id="update"
value="Save"
action="#{masterEditor.update}"
rendered="#{masterEditor.managed}"/>
<h:commandButton id="delete"
value="Delete"
action="#{masterEditor.remove}"
immediate="true"
rendered="#{masterEditor.managed}"/>
<s:button id="done"
value="Done"
propagation="end"
view="/Master.xhtml"
rendered="#{masterEditor.managed}"/>
<s:button id="cancel"
value="Cancel"
action="#{masterEditor.cancelChanges}"
propagation="end"
view="/#{empty masterFrom ? 'MasterList' : masterFrom}.xhtml"
rendered="#{masterEditor.managed}"/>
</div>
</h:form>
</ui:define>
</ui:composition>