3

I am trying to create a dynamic menu: a menu like those found on Amazon or eBay to browse categories. My first attempt is shown below:

The backing bean:

@ManagedBean
@ViewScoped
public class CategoryBackBean implements ActionListener {
    private MenuModel model;
    private Category category;

    public CategoryBackBean() throws IOException {
        category = Category.createRootCategory();
        createModel();
    }


    private void createModel() throws IOException {
        MenuModel tempModel = new DefaultMenuModel();
        for(Category c : category.getChildCategories()) {
            MenuItem childItem = new MenuItem();
            childItem.setValue(c.getName());
            childItem.addActionListener(this);
            tempModel.addMenuItem(childItem);
        }
        this.model = tempModel;
    }

    public MenuModel getModel() {
        return model;
    }

    @Override
    public void processAction(ActionEvent event) throws AbortProcessingException {
        try {
            MenuItem item = (MenuItem) event.getSource();
            String categoryName = (String) item.getValue();
            for(Category c : category.getChildCategories()) {
                if(c.getName().equals(categoryName)) {
                    category = c;
                    createModel();
                    return;
                }
            }
        } catch (IOException ex) {
            Logger.getLogger(CategoryBackBean.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

The webpage:

<h:body>
    <h:form>
        <p:menubar model="#{categoryBackBean.model}" />
    </h:form>
</h:body>

For starters, my design doesn't work: the initial menu is created, but when clicking on buttons, the menu is not recreated in sub-categories.

What is the best way to tackle this general problem? I'm not looking for quick hacks to get the above code working- I'm looking for a general design for a recursive menu.

Jean-François Corbett
  • 37,420
  • 30
  • 139
  • 188
Kevin
  • 4,070
  • 4
  • 45
  • 67
  • You should build your menu component dynamically in a `@SessionScoped` managed bean, then bind this menu to your ``. To create PrimeFaces components dynamically: [How to create Dynamic PrimeFaces menus](http://kevindoran1.blogspot.com/2012/02/dynamic-primefaces-menus-menu-menubar.html) – Luiggi Mendoza Nov 29 '12 at 05:43
  • 1
    lol, that's my own blog. I have had issues with that design. I am trying to move away from `@SessionScoped`, and want to utilise ajax (which the design you linked does not). – Kevin Nov 29 '12 at 08:22
  • I like that design. Could you please paste the content of Category Class or Interface. Thanks – Hanynowsky Jan 26 '13 at 15:08
  • The interface would include just the methods you see here. It will totally depend on context. See here for mode details: http://kevindoran1.blogspot.co.nz/2012/02/dynamic-primefaces-menus-menu-menubar.html – Kevin Jan 27 '13 at 03:06

4 Answers4

5

You need to update the menu after the action is completed. Given that you don't want to hardcode the menu ID, use @parent.

childItem.setUpdate("@parent");

An alternative, if the menu is the sole component in the form, is to just use @form.

Whether this all is the "best" way or not can't be objectively answered. If the code does exactly the job you want in the simplest possible and least intrusive way, then it's acceptable.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
0

Solution 1.

I think for the actionListener to be notified, there must first be an action event. So I tried adding this ugly code (surely there is a better way?) to add an action event to each childItem:

private void createModel() throws IOException {
    FacesContext facesCtx = FacesContext.getCurrentInstance();
    ELContext elCtx = facesCtx.getELContext();
    ExpressionFactory expFact = facesCtx.getApplication().getExpressionFactory();

    MenuModel tempModel = new DefaultMenuModel();
    for(Category c : childCategories) {
        MenuItem childItem = new MenuItem();
        childItem.setValue(c.getName());
        childItem.addActionListener(this);
        childItem.setActionExpression(expFact.createMethodExpression(elCtx, "#{categoryBackBean.doSomething()}", void.class, new Class[0]));
        tempModel.addMenuItem(childItem);
    }
    this.model = tempModel;
}

Where doSomething() is just a dummy method in order to get the action listener called.

This led to this issue with creating session after a committed response, which required this hack:

@PostConstruct
public void sessionStart() {
    FacesContext.getCurrentInstance().getExternalContext().getSession(true);
}

After all of which, I needed to disable Ajax on each element so that the form would be submitted:

childItem.setAjax(false);

So in summary, a hack chain, but it now functions (without ajax).

Community
  • 1
  • 1
Kevin
  • 4,070
  • 4
  • 45
  • 67
  • An alternative to disabling Ajax could be to add a Javascript action to the childItem: `childItem.setOnclick("document.forms[0].submit()");` – Kevin Nov 29 '12 at 22:31
0

Another solution, solution 2. This solution avoids the need for setting an action expression, however, it does introduce a backwards dependency on the XHTML code (in this case <p:menubar ..> must have an id of "menuid").

private void createModel() throws IOException {
    MenuModel tempModel = new DefaultMenuModel();
    for(Category c : childCategories) {
        MenuItem childItem = new MenuItem();
        childItem.setValue(c.getName());
        childItem.addActionListener(this);
        childItem.setUpdate("menuId");     // Magic new line.
        tempModel.addMenuItem(childItem);
    }
    this.model = tempModel;
}

This works by making every menu item cause the whole menubar to be updated via Ajax. If there was some way to get the menu's id without hard-coding...

Kevin
  • 4,070
  • 4
  • 45
  • 67
0

Solution 3. This solution is very simple. The backing bean:

@ManagedBean 
@ViewScoped 
public class CategoryBackBean implements Serializable {
    private Category category = Category.createRootCategory();
        public void setCategory(Category category) {
        this.category = category;
    }     

    public Category getCategory() { 
        return category;
  }
}

The web page:

<h:form>
    <p:menu id="myMenu">
        <c:forEach items="#{categoryBackBean.category.childCategories}" var="subCategory">
            <p:menuitem value="#{subCategory.name}" update="myMenu">
                <f:setPropertyActionListener target="#{categoryBackBean.category}" value="#{subCategory}" />
            </p:menuitem>
        </c:forEach>
    </p:menu>
</h:form>

Notable points:

  • <c:forEach> was used instead of <ui:repeat> as the latter doesn't work inside a Primefaces menu.

  • The update="myMenu" is set in the XHTML, and not in the backing code. This removes the issue with my Solution #2.

  • The design goes goes against a suggested Primefaces design heuristics, as hinted to here, which is that a backing model should be used instead of creating the menu with <ui:repeate> or <c:forEach> (I would argue against this, given the simplicity of the technique <c:forEach>).

Update

Do not use this answer! The <c:forEach/> 'runs' before the component tree is created, and is not re-run when the page is updated, either through Ajax or using a postback. The only reason it worked for me was due to the first category having the most sub-categories, and thus, it would initially create the component tree with enough <p:menuitems /> for any other category.

Kevin
  • 4,070
  • 4
  • 45
  • 67
  • I don't understand what exactly do you want. I have a `p:menu` in my app, which is loading every operation programatically and I have used `c:forEach` tag on it. As my menu works with non-Ajax calls I have no problem with that. It is true that `c:forEach` executes before nothing else from JSF, but if you have this attached to a `@SessionScoped` attribute an non Ajax on it, there is no problem. – Aritz Jan 12 '13 at 17:09