The Bean-Backed Table Design Pattern
Tue Jan 22 17:54:14 EST 2013
First off, I don't like the name of this that I came up with, but it'll have to do.
One of the design problems that comes up all the time in Notes/Domino development is the "arbitrary table" idea. In classic Notes, you could solve this with an embedded view, generated HTML (if you didn't want it to be good), or a fixed-size table with a bunch of hide-whens. With XPages, everything is much more flexible, but there's still the question of the actual implementation.
The route I've been taking lately involves xp:dataTable
s, List
s of Map
s, MIMEBean, and controller classes. In a recent game-related database, I wanted to store a list of components that go into a recipe. There can be an arbitrary number of them (well, technically, 4 in the game, but "arbitrary" to the code) and each has just two fields: an item ID and a count. The simplified data table code looks like this:
<xp:dataTable value="#{pageController.componentInfo}" var="component" indexVar="componentIndex"> <xp:column styleClass="addRemove"> <xp:this.facets> <xp:div xp:key="footer"> <xp:button value="+" id="addComponent"> <xp:eventHandler event="onclick" submit="true" refreshMode="partial" refreshId="componentInfo" action="#{pageController.addComponent}"/> </xp:button> </xp:div> </xp:this.facets> <xp:button value="-" id="removeComponent"> <xp:eventHandler event="onclick" submit="true" refreshMode="partial" refreshId="componentInfo" action="#{pageController.removeComponent}"/> </xp:button> </xp:column> <xp:column styleClass="itemName"> <xp:this.facets><xp:text xp:key="header" value="Item"/></xp:this.facets> <xp:inputText value="#{component.ItemID}"/> </xp:column> <xp:column styleClass="count"> <xp:this.facets><xp:text xp:key="header" value="Count"/></xp:this.facets> <xp:inputText id="inputText1" value="#{component.Count}" defaultValue="1"> <xp:this.converter><xp:convertNumber type="number" integerOnly="true"/></xp:this.converter> </xp:inputText> </xp:column> </xp:dataTable>
The two data columns are pretty normal - they could easily point to an array/List in a data context or a view/collection of documents. The specific implementation is that it's an ArrayList
stored in the view scope - either created new or deserialized from the document as appropriate:
public void postNewDocument() { Map<String, Object> viewScope = ExtLibUtil.getViewScope(); viewScope.put("ComponentInfo", new ArrayList<Map<String, String>>()); } public void postOpenDocument() throws Exception { Map<String, Object> viewScope = ExtLibUtil.getViewScope(); Document doc = this.getDoc().getDocument(); viewScope.put("ComponentInfo", JSFUtil.restoreState(doc, "ComponentInfo")); } public List<Map<String, Serializable>> getComponentInfo() { return (List<Map<String, Serializable>>)ExtLibUtil.getViewScope().get("ComponentInfo"); }
Those two "addComponent" and "removeComponent" actions are very simple as well: they just fetch the list from the view scope and manipulate it:
public void addComponent() { this.getComponentInfo().add(new HashMap<String, Serializable>()); } public void removeComponent() { int index = (Integer)ExtLibUtil.resolveVariable(FacesContext.getCurrentInstance(), "componentIndex"); this.getComponentInfo().remove(index); }
One key part to notice is that the "componentIndex" variable is indeed the value you'd want. In the context of the clicked button (say, in the second row of the table), the componentIndex
variable is set to 1, and the resolver and FacesContext
make that work.
On save, I just grab the modified Document
object, serialize the object back into it, and save the data source (that's what super.save()
from my superclass does):
public String save() throws Exception { List<Map<String, Serializable>> componentInfo = this.getComponentInfo(); Document doc = this.getDoc().getDocument(true); JSFUtil.saveState((Serializable)componentInfo, doc, "ComponentInfo"); return super.save(); }
The end result is that I have a user-expandable/collapsible table that can hold arbitrary fields (since it's a Map
- it could just as easily be a list of any other serializable objects):
If I wanted to access the data from non-Java contexts, I could replace the MIMEBean bits with another method, like a text-list-based format or response documents, but the final visual result (and the XSP code) would be unchanged.