My original post on this is here. It links to an Icefaces post which discusses using a decorator to do this. The fundamental problem is that you often want view state such as (but certainly not limited to) a selected flag on objects e.g. in tables. However, you do not want to pollute domain classes with view state, so it would not be desirable to add a selected flag to your domain objects as this blurs the layers (especially as you may have multiple different UI layers on the same domain, such as JSF browser clients, smart phone web clients, web services etc. all with different needs). Other examples of view state might be style classes to be used on different table rows in response to use actions etc.
Since my original post I have looked at other ways of achieving this, and summarize various approaches below which could be used to add view state to rows of a table.
In each case, assume initially we have a JSF table of the form <h:dataTable var=”row” value=”#{mainBean.rows}”>, where each row object has the properties firstName and surname, plus the need for a selected boolean flag to be maintained separately from the row.
1/ Using a row decorator
My original post discusses using a decorator to add the extra state, as per the icefaces post. This involves wrapping every row, but is transparent to the code. The downside of the decorator is that each domain class requiring the extra state needs its own decorator even though each case might require the same extra state. This potentially means a lot of extra classes just to add a selected flag.
2/ Using a generic row wrapper class composed with the row
A second alternative which is similar to the decorator would be just to use a wrapper class which holds the extra state plus the domain class instance, and explicitly make the calls to the domain class methods and properties. Thus, the following code might be used to read our table row properties and the selected flag (all the examples use direct instantiation for simplicity) :-
//wrap a row. rowWrappers is the list containing the wrapped rows
rowWrappers.add(new RowWrapper<Row>(row));
// read properties on a facelets page – note that the table now contains wrapped rows:-
<h:dataTable var=”rowWrapper” value=”#{mainBean.rowWrappers}”>,
<h:outputText value=”#{rowWrapper.row.firstName}”>
<h:outputText value=”#{rowWrapper.row.surname}”>
//read the selected flag on a facelets page
<h:outputText styleClass=”#{rowWrapper.selected ? ‘style1’ : ‘style2’}” value=”…”/>
The pros and cons of this approach are:-
- All the rows need to be wrapped as before.
- All the ‘delegation’ to access row properties is explicit in all the expressions.
- The upside however is that to add a given set of view state properties, only a single class is needed and this can wrap any number of domain classes generically, which can be a significant advantage.
3/ Using a Map to hold the additional row state objects
A third approach makes use of the fact that JSF EL value expressions can transparently read Map entries just like properties. Therefore where object m is a map or object that implements java.util.Map, the equivalent expressions m.property1 and m[‘property1’] when reading a value (i.e. on the right hand side of an expression, or rvalue mode) both result in
m.get(‘property1’)
being executed. When writing a value (i.e. on the left hand side of an expression, or in lvalue mode), as in m.property1 = rhsexpression, the statement
m.put(‘property1’, rhsexpression)
will be executed, assigning rhsexpression as the value of property1.
This technique allows us to use the row object itself as a key to the map containing the additional row state:-
//add row state object to map
rowFlagsMap.add(row, new RowFlags());
// read properties on a facelets page :-
<h:dataTable var=”row” value=”#{mainBean.rows}”>,
<h:outputText value=”#{row.firstName}”>
<h:outputText value=”#{row.surname}”>
//read the selected flag on a facelets page using the current row as a map key
<h:outputText styleClass=”#{mainBean.rowFlagsMap[row].selected ? ‘style1’ : ‘style2’}” value=”…”/>
Note that the nested expression evaluation works correctly with the map as the ‘man in the middle’ and that the expression mainBean.rowFlagsMap[row].selected could be used equally to fetch any other property from the rowflags object instead of the selected property. The expression works as we would expect in both lvalue and rvalue modes, such that if for example the above property value was written to as true from the jsf page (e.g. from the value property of an updated check box we had just ticked), the expression would execute correctly as
mainBean.rowFlagsMap.get(row).selected=true;
The pros and cons of this approach are:-
- We need an extra map access to get the row state, but for typical table sizes a hashmap would give very efficient access and should not be an issue.
- We need to use the more complex expression mainBean.rowFlagsMap[row].selected to fetch the additional row state. However, the plus is that we do not have to wrap each row in the code, and assuming that there are more table properties than additional state, we do not have to explicitly use a nested expression for them as we would in the previous approach.
- When using this approach, it must be born in mind that you are using an object as a Map key. This should not be an issue, but if you override the hashCode function such that the hash value could change during the lifetime of a row object (for example if a row property changed) you would need to take account of this. Note also that rows in the map will only correctly match the exact same objects. If you refetch the same domain objects again from your entity manager you cannot expect them to match any existing objects in the map. Again, this should not normally be an issue as you would expect to rebuild the map from scratch when refetching domain objects from persistent storage.
4/ Using a Smart Map Interface to implement a selected property by storing the selected row objects
This approach is similar to the previous one, in that it uses a Map interface to hold a selected flag. The difference is that we implement our own class implementing the Map interface and hide a map behind it. Our Map class simply stores each selected row in a ‘real’ map. When the caller reads the selected flag for a row by calling map.get on our map class, we perform a contains call on the ‘real’ map and return true if the row is present, or false if it is not. Similarly, when asked to set selected =true for a row, we just add the row to the ‘real’ map. When asked to set selected=false for a row, we remove it from the ‘real’ map. The pros and cons of this approach are:-
- We only need to store the selected rows in the map, so we are storing less map entries.
- The expressions to check the selected flag are a bit simpler, requiring something of the form mainBean.selectedMap[row]
- Conversely, we need to implement the smart map – this needs quite a number of dummy methods when implementing the Map interface. When implementing this, I typically create an abstract base class which dummies out most of the java.util.Map methods which I don’t need, and then extend this for a desired implementation. This project in the repository makes use of this technique – see the classes uk.co.salientsoft.view.JSFValueMap and uk.co.salientsoft.view.SelectedMap.
- The other downside is that this method only allows storage of a simple boolean based on the presence or absence of a row. If we need more additional row state, we need to look to one of the other methods.
- As with the previous method, we are using a row object itself as a map key, so the same caveats apply as in the previous case.
Conclusion
As is often the case, there is no clear winner – you take your pick depending on individual requirements and the various pros and cons. Overall, my current favourite in general is to use the additional Map. It is simple to implement (you don’t need to dummy a load of Map methods like you do with the smart Map). Unlike the smart Map, you can add any number of additional properties to a row. The domain row properties can be accessed exactly as before, and you don’t need to wrap every row (however you do need to create the row state object and add it to the map for every row). Whilst the access to the additional state requires a slightly more complex expression, this is likely to be needed less often than expressions to read the domain properties.