Mittwoch, 16. Juli 2014

Use XML menu model to create accordion with links for navigation

It's known that you can use a xml structure from trinidad to create a navigation model via a page-template. (http://docs.oracle.com/cd/E15523_01/web.1111/b31973/af_navigate.htm#autoId8).
With the help of that menu model you can use the component af:navigationPane to generate the different levels of navigation in:

bar

buttons

choice

list

tabs

This is really nice to handle at all because the consuming application only needs to create the xml and the navigatin structure will be generated at runtime.

Now, i had the task to generate something like this:

A Navigation with the first level navigation inside an accordion and the second level navigation with a vertical list of links (should be possible to extend this to third level also but here we only used two levels of navigation). The second task was that this should be switched in the template so every page using this template is working without (directly) touching it afterwards. So i was forced to use the xml menu model.Now the problem was that the af:accordion doesn't know the xml menu stuff. So i had to start playing ;-)The first thing i had to manage was the first level navigation (level 0). The xml menu was accessible via bindings because of the instantiation in the adfc-config (#{root_menu}). To get n-showDetailItems inside the accordion i used an af:iterator which can handle the first level of the menu structure! The JDeveloper doesn't like an af:iterator inside an af:accordion but at runtime it's working correctly. I also tried the af:foreach but this one doesn't like the xml menu strucutre so i decided to use af:iterator instead.

<af:panelAccordion id="pt_pa1"> <af:iterator id="iter1" value="#{root_menu}" var="menu0"> <af:showDetailItem text="#{menu0.label}" id="pt_sdi1" disabled="#{menu0.disabled}" visible="#{menu0.visible}" rendered="#{menu0.rendered}"></af:showDetailItem> </af:iterator></af:panelAccordion>
The next thing was the action behavior. The old template used the "doAction" property from the menu logic so i needed to use it also. Problem was that i do not have any "action" on a af:showDetailItem. The only event i can use is the disclosureListener. So i decided to create a hidden button next to each showDetailItem inside the af:iterator which has the doAction on the action property (which fires the navigation of the page finally). Ok. How to fire the button via the listener? i cannot use any bindings to a bean because of the iteration of the components. I have to find the correct button to the corresponding showDetailItem at runtime. So i walk through the DOM model via the listener component and queue the button actionevent:

for (int i = 0; i < children.size(); i++) { if (children.get(i) instanceof RichCommandButton) { RichCommandButton button = (RichCommandButton)children.get(i); ActionEvent actEvent = new ActionEvent(button); actEvent.queue(); } } }}Very Nice! Ok, but here comes the next problem. After navigation the whole page switches and the af:accordion shows the first showDetailItem all the time... Hmm, so we need to hold state of current open parts. But how to say every showDetailItem that it has a special state? They only exist at runtime! We need to set it indirectly via a bean with higher scope (used session scope here). We need a map of elements and state which is initialized in the constructor:private Map disclosed;public TemplateController() { XMLMenuModel menu = (XMLMenuModel)JsfUtils.resolveExpression("#{root_menu}"); int anzNodes = menu.getRowCount(); disclosed = new HashMap(); for (int i = 0; i < anzNodes; i++) { if (i == 0) { disclosed.put(i, true); } else { disclosed.put(i, false); } }

}Now we have a first list where the first item is open initially. Our disclosureListener needs to set it to the correct state (the current index we get via the varStatus of the af:iterator) and the showDetailItem needs to read out this state:public void showDetailDisclosureListener(DisclosureEvent event) { if (event.isExpanded()) { List<UIComponent> children = event.getComponent().getParent().getChildren(); int index = (Integer)JsfUtils.resolveExpression("#{menu0stat.index}"); for (int i = 0; i < disclosed.size(); i++) { if (i == index) { disclosed.put(index, true); } else { disclosed.put(i, false); } } for (int i = 0; i < children.size(); i++) { if (children.get(i) instanceof RichCommandButton) { RichCommandButton button = (RichCommandButton)children.get(i); ActionEvent actEvent = new ActionEvent(button); actEvent.queue(); } } }

</af:panelAccordion>What i realized at the end was a flickering of the accordion component after navigation because it was recreated for the next page - the smooth animation isn't working here. So i decided to disable animation for the application to have a better user experience:<?xml version="1.0" encoding="UTF-8"?><trinidad-config xmlns="http://myfaces.apache.org/trinidad/config"> <skin-family>my_skyros</skin-family> <skin-version>v1</skin-version> <animation-enabled>false</animation-enabled>