Making Those PanelBoxes Behave
- by Duncan Mills
I have a little problem to solve earlier this week - misbehaving <af:panelBox> components... What do I mean by that? Well here's the scenario, I have a page fragment containing a set of panelBoxes arranged vertically. As it happens, they are stamped out in a loop but that does not really matter. What I want to be able to do is to provide the user with a simple UI to close and open all of the panelBoxes in concert. This could also apply to showDetailHeader and similar items with a disclosed attrubute, but in this case it's good old panelBoxes.
Ok, so the basic solution to this should be self evident. I can set up a suitable scoped managed bean that the panelBoxes all refer to for their disclosed attribute state. Then the open all / close commandButtons in the UI can simply set the state of that bean for all the panelBoxes to pick up via EL on their disclosed attribute. Sound OK? Well that works basically without a hitch, but turns out that there is a slight problem and this is where the framework is attempting to be a little too helpful. The issue is that is the user manually discloses or hides a panelBox then that will override the value that the EL is setting. So for example.
I start the page with all panelBoxes collapsed, all set by the EL state I'm storing on the session
I manually disclose panelBox no 1.
I press the Expand All button - all works as you would hope and all the panelBoxes are now disclosed, including of course panelBox 1 which I just expanded manually.
Finally I press the Collapse All button and everything collapses except that first panelBox that I manually disclosed.
The problem is that the component remembers this manual disclosure and that overrides the value provided by the expression. If I change the viewId (navigate away and back) then the panelBox will start to behave again, until of course I touch it again! Now, the more astute amoungst you would think (as I did) Ah, sound like the MDS personalizaton stuff is getting in the way and the solution should simply be to set the dontPersist attribute to disclosed | ALL. Alas this does not fix the issue.
After a little noodling on the best way to approach this I came up with a solution that works well, although if you think of an alternative way do let me know. The principle is simple. In the disclosureListener for the panelBox I take a note of the clientID of the panelBox component that has been touched by the user along with the state. This all gets stored in a Map of Booleans in ViewScope which is keyed by clientID and stores the current disclosed state in the Boolean value.
The listener looks like this (it's held in a request scope backing bean for the page):
public void handlePBDisclosureEvent(DisclosureEvent disclosureEvent) {
String clientId = disclosureEvent.getComponent().getClientId(FacesContext.getCurrentInstance());
boolean state = disclosureEvent.isExpanded();
pbState.addTouchedPanelBox(clientId, state);
}
The pbState variable referenced here is a reference to the bean which will hold the state of the panelBoxes that lives in viewScope (recall that everything is re-set when the viewid is changed so keeping this in viewScope is just fine and cleans things up automatically). The addTouchedPanelBox() method looks like this:
public void addTouchedPanelBox(String clientId, boolean state) {
//create the cache if needed this is just a Map<String,Boolean>
if (_touchedPanelBoxState == null) {
_touchedPanelBoxState = new HashMap<String, Boolean>();
}
// Simply put / replace
_touchedPanelBoxState.put(clientId, state);
}
So that's the first part, we now have a record of every panelBox that the user has touched. So what do we do when the Collapse All or Expand All buttons are pressed? Here we do some JavaScript magic. Basically for each clientID that we have stored away, we issue a client side disclosure event from JavaScript - just as if the user had gone back and changed it manually.
So here's the Collapse All button action:
public String CloseAllAction() {
submitDiscloseOverride(pbState.getTouchedClientIds(true), false);
_uiManager.closeAllBoxes();
return null;
}
The _uiManager.closeAllBoxes() method is just manipulating the master-state that all of the panelBoxes are bound to using EL. The interesting bit though is the line:
submitDiscloseOverride(pbState.getTouchedClientIds(true), false);
To break that down, the first part is a call to that viewScoped state holder to ask for a list of clientIDs that need to be "tweaked":
public String getTouchedClientIds(boolean targetState) {
StringBuilder sb = new StringBuilder();
if (_touchedPanelBoxState != null && _touchedPanelBoxState.size() > 0) {
for (Map.Entry<String, Boolean> entry : _touchedPanelBoxState.entrySet()) {
if (entry.getValue() == targetState) {
if (sb.length() > 0) {
sb.append(',');
}
sb.append(entry.getKey());
}
}
}
return sb.toString();
}
You'll notice that this method only processes those panelBoxes that will be in the wrong state and returns those as a comma separated list.
This is then processed by the submitDiscloseOverride() method:
private void submitDiscloseOverride(String clientIdList, boolean targetDisclosureState) {
if (clientIdList != null && clientIdList.length() > 0) {
FacesContext fctx = FacesContext.getCurrentInstance();
StringBuilder script = new StringBuilder();
script.append("overrideDiscloseHandler('");
script.append(clientIdList);
script.append("',");
script.append(targetDisclosureState);
script.append(");");
Service.getRenderKitService(fctx, ExtendedRenderKitService.class).addScript(fctx, script.toString());
}
}
This method constructs a JavaScript command to call a routine called overrideDiscloseHandler() in a script attached to the page (using the standard <af:resource> tag). That method parses out the list of clientIDs and sends the correct message to each one:
function overrideDiscloseHandler(clientIdList, newState) {
AdfLogger.LOGGER.logMessage(AdfLogger.INFO, "Disclosure Hander newState " + newState + " Called with: " + clientIdList);
//Parse out the list of clientIds
var clientIdArray = clientIdList.split(',');
for (var i = 0; i < clientIdArray.length; i++){
var panelBox = flipPanel = AdfPage.PAGE.findComponentByAbsoluteId(clientIdArray[i]);
if (panelBox.getComponentType() == "oracle.adf.RichPanelBox"){
panelBox.broadcast(new AdfDisclosureEvent(panelBox, newState));
}
}
}
So there you go. You can see how, with a few tweaks the same code could be used for other components with disclosure that might suffer from the same problem, although I'd point out that the behavior I'm working around here us usually desirable.
You can download the running example (11.1.2.2) from here.