Thursday, 25. January 2007, 14:12:45
The ADF Developer's Guide (
link) contains a lot of useful sections on implementing different features. One of them is how to model a dynamic menu structure (
19.2 Using Dynamic Menus for Navigation).
This chapter shows how to take advantage of the af: page nodeStamp facet to achieve a multi-level menu system based on managed beans. This has three levels of menu - the top level is represented by items in a menuTabs component, the second level by items in a menuBar component and the third level by items in panelSideBar. After that, deeper levels are represented as nodes on a menuTree component inside the panelSideBar. And the great thing about this is that for the current page, all the right items in the tree are highlighted, showing the menu path.
There are two problems I see with this. The first is that at three levels, the elements in the panelSideBar are bulleted, whereas at four or more levels, the bullets disappear as the list is replaced with a menuTree. This can make the items harder to differentiate (is that one item that has gone onto two lines, or is it two separate items?).
The second problem is that you may have pages that you don't want to show up on the menu system, but you do want to see the menu path to the page. For example, I have a series of pages that allow you to create/edit an item in a list. I don't want these pages to show up in the menu system because I want the user to only have one access point to them (the create/edit buttons on the list page). I do, however, want to show the user what area of the application they are in.
For me both of these problems happened at the same level in the menu system (my create/edit pages are on the fourth level). I needed a way of curbing the shown level of the menu tree, but at the same time not sacrificing the highlighted menu path functionality.
To do this, I have added a maxLevel attribute to the MenuTreeModelAdapter and subclassed both ChildPropertyTreeModel and ViewIdPropertyMenuModel. First, the ChildPropertyTreeModel subclass.
The code for all of these classes is based on the code in the Trinidad repository.MaxLevelChildPropertyTreeModel
(imports)
public class MaxLevelChildPropertyTreeModel extends ChildPropertyTreeModel
{
private int maxLevel;
public MaxLevelChildPropertyTreeModel(Object instance, String childProperty, int maxLevel)
{
super(instance, childProperty);
this.maxLevel = maxLevel;
}
public boolean isContainer()
{
boolean container = super.isContainer() && (getDepth() < maxLevel);
return container;
}
public boolean isReallyContainer()
{
boolean container = super.isContainer();
return container;
}
public boolean isContainerReallyEmpty()
{
if (!isReallyContainer()) return true;
enterContainer();
try
{
int kids = getRowCount();
if (kids < 0)
{
setRowIndex(0);
return !isRowAvailable();
}
return (kids == 0);
}
finally
{
exitContainer();
}
}
}
As you can see, there is a maxLevel attribute passed in the constructor. This comes from the value in the MenuTreeModelAdapter. I have overridden the
isContainer method to return false if the current depth is greater or equal to the maxLevel. This ensures that all nodes of the menu tree at that level are considered empty. This results in the menu being rendered to only
maxLevel levels, which for me is 2 (where 0 is the top level).
The other two methods are for the benefit of our MaxLevelViewIdPropertyMenuModel, so that it can traverse the whole tree to find all the pages. This is so when the application requests the row key of the current page, there will be an entry in the list. It doesn't matter how long the row key is, the correct menu path will be highlighted (i.e. using a maxLevel of 2, the row key [1, 0, 3, 0, 1] will highlight [1], [1, 0] and [1, 0, 3] in the menu system.
MaxLevelViewIdPropertyMenuModel
(imports)
public class MaxLevelViewIdPropertyMenuModel extends ViewIdPropertyMenuModel
{
private Map<Object, Object> _focusPathMap;
public MaxLevelViewIdPropertyMenuModel(Object instance, String viewIdProperty) throws IntrospectionException
{
super(instance, viewIdProperty);
}
public void setWrappedData(Object data)
{
Object oldPath = getRowKey();
//set the focus path map
_focusPathMap = new HashMap<Object, Object>();
_focusPathMap.clear();
setRowKey(null);
FacesContext context = FacesContext.getCurrentInstance();
_addToMap(context, (MaxLevelChildPropertyTreeModel)data, _focusPathMap, getViewIdProperty());
setRowKey(oldPath);
}
private static void _addToMap(
FacesContext context,
MaxLevelChildPropertyTreeModel tree,
Map<Object, Object> focusPathMap,
String viewIdProperty
)
{
for ( int i = 0; i < tree.getRowCount(); i++)
{
tree.setRowIndex(i);
if (viewIdProperty != null)
{
Object focusPath = tree.getRowKey();
Object data = tree.getRowData();
PropertyResolver resolver =
context.getApplication().getPropertyResolver();
Object viewIdObject = resolver.getValue(data, viewIdProperty);
focusPathMap.put(viewIdObject, focusPath);
}
if (tree.isReallyContainer() && !tree.isContainerReallyEmpty())
{
tree.enterContainer();
_addToMap(context, tree, focusPathMap, viewIdProperty);
tree.exitContainer();
}
}
}
public Object getFocusRowKey()
{
String currentViewId = getCurrentViewId();
Object focusRowKey = _focusPathMap.get(currentViewId);
return focusRowKey;
}
}
This is slightly more complex. There is nothing different with the constructor - the values are simply passed on to the superclass constructor. In the superclass constructor, the
setWrappedData method is called, so we override that with our own version. This calls the
_addToMap method in the class rather than the
_addToMap method in the superclass. This method takes
MaxLevelChildPropertyTreeModel rather than a
TreeModel. This is so that we can call the
isReallyContainer and
isContainerReallyEmpty methods to get the real return values of each. This means that the real menu tree is mapped to the
_focusPathMap attribute. We then override the
getFocusRowKey to return the value from the map.
This all means that the correct row key will be return for all pages in the menu tree, even if they are not rendered.
The last thing to do is add the
maxLevel attribute to
MenuTreeModelAdapter and change the model classes in
MenuTreeModelAdapter and
MenuModelAdapter to our new classes.
MenuModelAdapter
public class MenuModelAdapter implements Serializable {
private String _propertyName = null;
private Object _instance = null;
private transient MenuModel _model = null;
private List _aliasList = null;
public MenuModel getModel() throws IntrospectionException
{
if (_model == null)
{
MaxLevelViewIdPropertyMenuModel model =
new MaxLevelViewIdPropertyMenuModel(getInstance(),
getViewIdProperty());
...
The important part is where the
model attribute is populated with an instance of
MaxLevelViewIdPropertyMenuModel.
MenuTreeModelAdapter
public class MenuTreeModelAdapter {
private String _propertyName = null;
private Object _instance = null;
private transient TreeModel _model = null;
private int _maxLevel = Integer.MAX_VALUE;
public TreeModel getModel() throws IntrospectionException
{
if (_model == null)
{
_model = new MaxLevelChildPropertyTreeModel(getInstance(), getChildProperty(), getMaxLevel());
}
return _model;
}
public void setMaxLevel(int maxLevel)
{
this._maxLevel = maxLevel;
}
public int getMaxLevel()
{
return _maxLevel;
}
...
This shows the
maxLevel attribute and how
_model is populated with an instance of
MaxLevelChildPropertyTreeModel.
And that's pretty much it. This is set up so that there's one global maximum level, but it could be improved upon to have custom levels for each part of the menu tree, say 2 levels here and 5 levels there.
I have included a JDeveloper application which has all of this code and a few pages that demonstrated the solution. Unzip it into your JDeveloper mywork folder.
JOracle.zip