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:

public void showDetailDisclosureListener(DisclosureEvent event) {
   if (event.isExpanded()) {
       List<UIComponent> children =
           event.getComponent().getParent().getChildren();

       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();
            }
        }
    }

}

The second level of navigation (level=1) used the default navigationPane with "list" behavior - so this one was easy.

<af:panelAccordion id="pt_pa1">
  <af:iterator id="iter1" value="#{root_menu}" var="menu0" varStatus="menu0stat">
    <af:group id="gr1">
      <af:commandButton action="#{menu0.doAction}" 
                        visible="false" id="cmdbt1"
                        text="hiddenAction"/>
      <af:showDetailItem text="#{menu0.label}" id="pt_sdi1" 
                         disabled="#{menu0.disabled}"
                         visible="#{menu0.visible}" 
                         rendered="#{menu0.rendered}"
                         disclosureListener="#templateAktionen.                                                                 showDetailDisclosureListener}"
                         disclosed="#{templateAktionen.
                                      disclosedMap[menu0stat.index]}">
        <af:panelGroupLayout id="pt_pgl5" layout="scroll">
          <af:spacer width="10" height="10" id="pt_s6"/>
          <af:navigationPane hint="list" value="#{root_menu}" level="1"                                        var="menu1" id="pt_np2">
            <f:facet name="nodeStamp">
              <af:commandNavigationItem text="#{menu1.label}" 
                                        action="#{menu1.doAction}"
                                        disabled="#{menu1.disabled}"
                                        destination="#{menu1.destination}"
                                        visible="#{menu1.visible}" 
                                        rendered="#{menu1.rendered}"
                                        id="pt_cni2"/>
            </f:facet>
          </af:navigationPane>
        </af:panelGroupLayout>
      </af:showDetailItem>
    </af:group>
  </af:iterator>

</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>

</trinidad-config>

Works like a charme :-)

Mittwoch, 26. März 2014

Avoid double download of PDF in popup + inlineframe

After some research i realized that my inlineFrame which renders a pdf in a popup downloads the file twice. The first time after the popup opens and the second time after close. (hint: the popup has content delivery "lazyUncached")

I tried different ways to close the popup:

  • The dialog shows closeIcon which closes the popup
  • A button next to the inlineFrame with partialSubmit resets the url and closes the popup
  • Autoclose enabled
But it was not possible to avoid the second request on close event. I also tried to set the url of the inlineFrame to null before i close the popup with a button - i don't know how but the same request as before was fired again.

Finally i had to avoid the whole popup to have a good result.
Instead of using a real popup i created a BTF based on pages with one page in it:

<f:view>
    <af:document id="d1"
                 title="Online Postfach Mitteilung">
      <af:form id="f1">
        <af:panelStretchLayout id="psl1" bottomHeight="40px">
          <f:facet name="center">
            <af:inlineFrame id="if1" sizing="preferred"
                    source="#{myBean.URL}"/>
          </f:facet>
        </af:panelStretchLayout>
      </af:form>
    </af:document>
  </f:view>


I embed this BTF as TF-Call in my BTF of the application and set the option to run as dialog:


After that i only needed a button to trigger my TF control flow case "showPdf" in combination with UseWindow=true.


Finally i had the exact same result but my close "X" from dialog avoids the second download of the pdf via the servlet in my inlineFrame.


Every kind of close (i use the X here) simply closes the popup dialog and its BTF completely - no second request after all. :-)