Monday Apr 15, 2013

Building the revealPanel - Part 2

In the first part of this series I described the basic physical makeup of the revealPanel and described a simple server side approach to the display and hide of the information panel and indicator triangle.  

In this posting I want to look at an alternative approach that uses the same physical components in the UI, but rather than handling the reveal with a roundtrip to the server, does it all in the browser itself using JavaScript.

The sample that you can download shows both approaches. 

Changing the Configuration for JavaScript Use

The JavaScript version of the component works in essentially the same way as the server side version that I talked about last time, that is, it simply sets the visible attribute of the panelGroupLayouts that contain the reveal information and the triangle. The good thing is at this is a property that we can set through the ADF Faces client component JavaScript API and we never really have to tell the server side what the state of the panels is as we'll always revert to the all-closed layout if anything like navigation away and back takes place. 

So the definition of say the triangle PGL simply has the visible attribute set to a hardcoded value of false, with no need to refer to a managed bean. Of course we need to hold the information about which panels are expanded somewhere,  so that we can close one set as another is opened, or close a set if it is currently open. To do this I actually create a new JavaScript object at the page level called revealPanelGroupMap (if you want to read along open the revealPanel.js file in the demo). 

This Object does not directly hold the information about the panel, it's a slightly more complex structure:

  1. revealPanelGroupMap is a Map (thinking in Java terms) of revealGroup Objects 
  2. Each RevealGroup contains an array of Topics and the currently selected Topic in the array
  3. Each Topic contains component references to the triangle PGL and the revealPanel PGL as well as the topic number
The fact that revealPanelGroupMap can hold multiple revealGroups allows us to host multiple revealPanels on the same page as long as each is identified by a unique string which is used to key that revealPanelGroupMap.

How are the Data Structures Created? 

So this revealPanelGroupMap does not appear out of thin air, but neither in fact is it created in advance in any way. Because, for an individual page, you may have dynamic regions and suchlike, I wanted the revealPanels to be self registering and created on demand rather than pre-defined in some way.  The way that I ended up achieving this was to add a client attribute to each of the three important panelGroupLayouts (the topic, the triangle and the revealPanel). This locator is just a string consisting of 3 parts delimited by ":".

  1. The panelGroupId - e.g. "REVEAL1"
  2. The TopicID - zero indexed
  3. The SegmentID - each topic has three segments 0-2 where 0 is the topic, 1 is the triangle and 2 is the revealPanel

So the client attribute definition in the page looks like this:

<af:clientAttribute name="revealPanelLocator" value="REVEAL1:0:0"/>

This indicates that this panel is the topic panel for the first topic in the REVEAL1 group. 

When the first of the topics is clicked within a revealPanel, the click handler code grabs this locator and parses out the segments.  From the first segment it can tell if this group has been seen before, if it has then great, it can start to set visibility etc. If, however, the group is not present in the main map then we do a little self discovery to find out how many Topics there are in the revealPanel group as a whole and store the relevant component references.

Walking the Tree

So how to we self discover the group?  Well the first bit is easy.  Using the getParent() API  from the component that raised the click event we can get a reference to the hosting panelGridLayout.  

I then use the visitChildren() api on the grid to visit each of its top level children. Each of these will actually be one of those segment panelGroupLayouts. So in a panelGrid with 9 children that will actually define 3 topics each consisting of three panels (topic, triangle, reveal) .  As each of those panels has an associated clientAttribute with it's locator we can add it to the correct place in the revealGroup control structure. 

The visitChildren API actually defines a callback function that the framework will call for you, passing any context that you've defined and the child component it has found. 

Note, it would be perfectly possible to alter the sample code to do away with the clientAttribute all together. I actually wrote it this way to allow for more than one revealPanel to be hosted within the same physical panelGrid. However, of that's not something you need then the page definition could be condensed down to simply the clientListener in the Topic panel.  Left as an exercise for the reader... 

Acting on the Click

Once the data structure is build for a particular revealPanel it will be caching the component references to the triangle and to the reveal PGL so you can simply call setVisible(true|false) to manage the hide and display.

You've Noticed the Fatal Flaw?

Well I hope so... If I'm storing the component references to the various panelGroupLayouts will they stay valid? Well certainly if I navigate off to another page no.  But that's OK, because if I navigate away then the document object in the DOM will have been destroyed, taking my data structures with it. So when I re-enter the page they just get re-built.  So no problems there.   However, if my page contains a region and one of the fragments on the region has a revealPanel then there is a problem.  Changing the view in the region will not effect the overall document so the control structures contain references to components that have been destroyed - if I go away and come back to a revealGroup on a fragment then the references will be bad.  However,  there is a simple solution to this.  The client side components have an isDead() API call which we can use to check to see if the reference held in the cache is still valid.  If it is then great. If not we assume that we need to rebuild the entire structure for that revealPanel.

Adding Some Bling with Animations

I promised some animation as part of this version and indeed it's in there.  So in fact, rather than simply setting the visible attribute when the topic is clicked I do that, but I also apply some CSS styles as well.  

I'm essentially re-using some of the things I've discussed before in my postings on animations, using in this case Scale transforms for the main reveal panel and a color transform for the triangle.

Here's the logical flow:

Revealing a Topic

  1. Set the triangle to visible - it's initial style will define it as transparent
  2. Set the reveal panel as visible  - its initial style will define a scale of (1,0) with full width but no height
  3. Set the reveal panel style to revealPanelAnimated. This resets the scale to (1,1) - full size over about 200ms
  4. Register a listener on the animation
  5. When the animation is finished then reset the style on the triangle to restore its color.

Note that I've used a scale transform here rather than actually trying to move the PGL <div> around the screen. Trying the latter could put you into conflict with the existing layout management and will certainly cause some problems with z-order in the DOM so it's better to avoid the issue by scaling in place.  It gives a pleasing effect.  

The actual code and styles involved in all of this is a little too complex to reproduce here in the blog, so do be sure to check out the demo.  I've added plenty of comments to help you along.  

Friday Apr 12, 2013

Building the revealPanel - Part 1

Working with one of our internal teams the other day, a couple of interesting ideas came up for presenting information. One of these was a sort of twist on panelAccordion and it seems to be a nice usecase to work through to show how you can get creative with your UI without having to ask for and wait for new components. 

The Brief

As I said, the requirement here was to produce something like panelAccordion. However, rather than restricting the selection bar to just a single line of text an maybe an icon,  what if you wanted larger images or multiple lines?  Here's a mockup:



Then like a panelAccordion, with discloseMany="false" and discloseNone="true" you want to be able to expand the details of up to one and only one of the selection panels (I'll call them "Topics" from now on).



So if I click anywhere in the first topic bar in the above image then that topic will collapse, if I click on a different topic bar then the first will collapse and the new selection will expand.
You'll notice the modernist conceit of the little triangle linking the revealed panel with it's topic. It's a small thing but that also helps to polish the look and make it feel current.

The UI Elements

So how would we build this? Well the answer is that the UI itself is surprisingly easy. That's all thanks to our new friend, the panelGridLayout  which hands us everything we need on a plate. 
We can break down the mockup above into three sets of repeating units, remember I called them topics.  Each topic is identical, so let's just look at one.
Logically the topic is composed of three vertically arranged areas:

(1) The topic header itself
        |
(2) The little indicator triangle
        |
(3) The reveal panel area with the additional content 

Area 1, the header is always displayed and areas 2 and 3 we need to reveal or hide dynamically when the user clicks anywhere on area 1.
In panelGridLayout terms we can simply translate this into three grid rows, one for each area.  The key point here is that if you set the height attribute of a <af:gridRow> to the value "auto" the grid will automatically size that row to fit its children, so guess what: if we set visible="false" on the contents in areas 2 and 3 then the grid shrinks for free. I just love that this is so easy.
So here's the outline component model for a revealPanel (Yes there is a full demo you can download - read on to the end)

<panelGridLayout>
<!-- For Each Topic -->
  <gridRow height="50px">
    <gridCell columnSpan="3" halign="stretch">
      <panelGroupLayout layout="vertical" styleClass="revealTopicPanel">
      <!-- Topic header content will go here --> 
      <panelGroupLayout>
    </gridCell>
  </gridRow>
  <gridRow height="auto">
    <gridCell width="80px"/>
    <gridCell width="10px" valign="bottom" >
      <panelGroupLayout layout="vertical" 
                        styleClass="triangleMarker"/>
    </gridCell>
    <gridCell width="100%"/>
  </gridRow>
  <gridRow height="auto">
    <gridCell columnSpan="3" halign="stretch">
      <panelGroupLayout styleClass="revealPanel"
      <!-- Revealed content will go here --> 
    </gridCell>
  </gridRow>
</panelGridLayout>

Let's look at the key points
  1. The first grid row representing the topic header can be fixed in size to make sure that the unexpanded list is regular.
  2. The first grid row just contains a single grid cell that streches across three logical cells using the columnSpan attribute. The reason for doing this will become clear when we look at the second area of the topic 
  3. We use halign="stretch" on that first area gridCell to fill it with its content. This is important to make the included component (e.g. panelGroupLayout in this case) fill the cell with its styling.
  4. Moving onto the second area of the topic than contains the little indicator triangle, here the row as it's height as auto so it will collapse when we hide the triangle.
  5. The triangle area row is divided into three cells. The center cell contains the actual triangle (see below) and the other two cells are used to position the triangle cell to a particular horizontal position. In this case I'm indenting the triangle by a fixed 80 pixels, however, if I adjusted the first and last cells to use width="50%" then the triangle would be centered to the width of the panel.
  6. The final area is again a grid row with height set to auto containing a single cell with columnSpan set to 3 and stretching it's content. 

The Triangle

The little triangle indicator could be done using an image file, however, here I'm just using a CSS style which is simpler and enables you to change the color as required. Here's the definition:

 .triangleMarker {
  width:0px;
  height:0px;
  border-left:8px solid transparent;
  border-right:8px solid transparent;
  border-bottom:8px solid rgb(247,255,214);  
}
This style is applied to the vertical panelGridLayout in the center area.  Vertical panelGrids become simple html <div> elements.

Managing the Topic - The Server Side Version

I'll cover a more advanced version of the component in my next posting which uses JavaScript to manage the hide and reveal of the panels, but in this post, let's look at the simple version. 
As discussed, the paneLGridLayout actually does all of the UI resizing for us, so all we really need to do to manage the set of topics is two things:
  1. Introduce a management class that will tell the reveal panel and triangle panel if they should be visible or not
  2. A small amount of event code to translate a click anywhere on the topic panel into a change in the above management class. 

The Management Class

For the sake of the demo I've developed a very simple management class that does not attempt to do anything fancy such as handling multiple sets of revealPanels (that task is left for the JavaScript implementation). Here's the class:

package oracle.demo.view;
import java.util.ArrayList;
import java.util.List;
public class RevealPanelManager {
    private int _panelCount = 10;
    private int _toggleTarget = -1;
    private List<Boolean> _revealedList;

    /**
     * Switches the state of the currently selected panel 
     */
    public void toggleState() {
        if (_toggleTarget >= 0 && _panelCount > 0) {
            boolean currentState = false;
            if (_revealedList != null){
                currentState = _revealedList.get(_toggleTarget);
            }
            resetStates();
            if (!currentState) {
                _revealedList.set(_toggleTarget, true);
            }
            _toggleTarget = -1;
        }
    }
    /**
     * Used to inject a panelCount into the management structure. If not called then an array upper limit of 
     * 10 will be used
     * @param panelCount
     */
    public void setPanelCount(Long panelCount) {
        int candidateCount = panelCount.intValue();
        if (candidateCount > 0) {
            //reset the list & re-create in the new size
            _revealedList = null;
            _panelCount = candidateCount;
            resetStates();    
        }
    }
    /**
     * Invoked, probably from a setPropertyListener / setActionListener to set the id of the 
     * panel to act on.  This may disclose or hide depending on the current state of the selected
     * panel
     * @param toggleNo - index number of the panel
     */
    public void setToggleTarget(int toggleNo) {
        this._toggleTarget = toggleNo;
    }
    /**
     *Called by the panel to see if it should be visible or not
     * @return List of Booleans indexed by panel number
     */
    public List<Boolean> getRevealed() {
        return _revealedList;
    }
    /**
     * Either creates or reinitializes the array to close all the 
     * panels
     */
    private void resetStates() {
        if (_revealedList == null) {
            _revealedList = new ArrayList<Boolean>(_panelCount);
            for (int i=0; i < _panelCount; i++) {
              _revealedList.add(i,false);
            }
        }
        else{
            for (int i = 0; i < _panelCount; i++) {
                _revealedList.set(i, false);
            }            
        }
    }    
}

As I said this class only manages a single set of topics in a single grid and it defined as a managed bean in your page flow definition for the relevant view something like this:

  <managed-bean>
    <managed-bean-name>revealManager</managed-bean-name>
    <managed-bean-class>oracle.demo.view.RevealPanelManager</managed-bean-class>
    <managed-bean-scope>view</managed-bean-scope>
    <managed-property>
      <property-name>panelCount</property-name>
      <property-class>java.lang.Long</property-class>
      <value>3</value>
    </managed-property>
  </managed-bean>
Notice how the expected panel count (3) in this case  is injected into this bean.

Once this management bean is defined then the content that we want to hide and display dynamically (e.g. the triangle panelGrouplayout and the revealPanel panelGrouplayout) and both use expression language to determine if they should be visible. e.g. 

<af:panelGroupLayout layout="vertical" 
                     styleClass="triangleMarker"
                     visible="#{viewScope.revealManager.revealed[0]}"/>
Where the zero based index number is passed into the list evaluation. 

The Click Event Handler

The final thing we need to do is to find a way to call the toggleState() method in the revealManager. The only twist here is that we don't want to have a commandButton or link in the panel to click. We want to be able to click anywhere on the panel.
This is a well established technique so you can look at the demo for the details of the code, but basically we define a small JavaScript function which is registered with the client side click. When the user clicks, this reads the numerical ID of the panel that was selected and makes a call back to the server to the handlePanelToggle() server listener method. That then talks to the revealManager, setting the id of the panel to toggle and invoking the toggle, finishing off with a partialUpdate to get the whole grid to re-draw. Have a look at the RevealRowComponentHandler.java class in the demo for the details.  

And the Result

Well here's the ADF implementation of the revealPanel. First closed:


And now opened:



The Demo

I've uploaded a demo that shows the basic version of revealPanel discussed here and a more advanced version which is discussed in the next article.  You can download the sample from the ADF Code Samples Repository

This demo is not foolproof and at the moment the animations are restricted to Chrome and Safari (WebKit browsers) It also needs to check for older browser versions that do not support the animations and switch them off

Note: The sample is written in 11.1.1.7 it will not work in 11.1.1.6 or older.

Further Note: There seems to have been a slight change in the js API in 11.1.2.n which are breaking the code at the moment - working on that one

Thursday Dec 20, 2012

Dude, how big's my browser?

It's sometimes funny how the same question comes from several unrelated places at almost the same time. I've had just such a question from a couple of unrelated projects recently, the query being the simple one of "how big is my browser window"?

Now in many cases this is a null question. With the correct use of layout containers (and testing) you can build layouts in ADF which adapt, flow and stretch well as the user re-sizes the browser. However, there are circumstances, as was the case here, where you want more drastic changes based in the amount of space you have to play with.  For example you may choose to hide a column in a table if the screen is below a certain size.

Well this is certainly possible, but of course it comes at a price.  If you want to know when the browser window has been resized you have to tell the server, and that's a round trip. So to do this efficiently this is not totally trivial. However, to save you the trouble of thinking too hard, I've written a sample - it is Christmas after all and Christmas is all about giving. 

The sample can be downloaded from the ADF Samples site on Java.net.

 

It's all pretty well documented  so I won't explain the code line by line here, but needless to say there is a bit of JavaScript and a server side event handler to listen for events queued from that script. I use a configurable JavaScript timer to buffer the resize events and keep the number of notifications to the server to an acceptable level. 

Once you have the  sizing information from the client, of course it's up to you to decide what to do with it!

The sample is written in 11.1.1.6 and will work for 11.1.1.n and 11.1.2.n versions. 

Wednesday Nov 14, 2012

Controlling the Sizing of the af:messages Dialog

Over the last day or so a small change in behaviour between 11.1.2.n releases of ADF and earlier versions has come to my attention. This has concerned the default sizing of the dialog that the framework automatically generates to handle the display of JSF messages being handled by the <af:messages> component. Unlike a normal popup, you don't have a physical <af:dialog> or <af:window> to set the sizing on in your page definition, so you're at the mercy of what the framework provides. In this case the framework now defines a fixed 250x250 pixel content area dialog for these messages, which can look a bit weird if the message is either very short, or very long. Unfortunately this is not something that you can control through the skin, instead you have to be a little more creative.

Here's the solution I've come up with.  Unfortunately, I've not found a supportable way to reset the dialog so as to say  just size yourself based on your contents, it is actually possible to do this by tweaking the correct DOM objects, but I wanted to start with a mostly supportable solution that only uses the best practice of working through the ADF client side APIs.

The Technique

The basic approach I've taken is really very simple.  The af:messages dialog is just a normal richDialog object, it just happens to be one that is pre-defined for you with a particular known name "msgDlg" (which hopefully won't change). Knowing this, you can call the accepted APIs to control the content width and height of that dialog, as our meerkat friends would say, "simples" 1

The JavaScript

For this example I've defined three JavaScript functions.  

  1. The first does all the hard work and is designed to be called from server side Java or from a page load event to set the default.
  2. The second is a utility function used by the first to validate the values you're about to use for height and width.
  3. The final function is one that can be called from the page load event to set an initial default sizing if that's all you need to do.

Function resizeDefaultMessageDialog()

/**
   * Function that actually resets the default message dialog sizing.
   * Note that the width and height supplied define the content area
   * So the actual physical dialog size will be larger to account for
   * the chrome containing the header / footer etc.
   * @param docId Faces component id of the document
   * @param contentWidth - new content width you need 
   * @param contentHeight - new content height
   */
  function resizeDefaultMessageDialog(docId, contentWidth, contentHeight) {
    // Warning this value may change from release to release
    var defMDName = "::msgDlg";
 
    //Find the default messages dialog
    msgDialogComponent = 
      AdfPage.PAGE.findComponentByAbsoluteId(docId + defMDName); 

    // In your version add a check here to ensure we've found the right object!

    // Check the new width is supplied and is a positive number, if so apply it.
    if (dimensionIsValid(contentWidth)){
        msgDialogComponent.setContentWidth(contentWidth);
    }
 
    // Check the new height is supplied and is a positive number, if so apply it.
    if (dimensionIsValid(contentHeight)){
        msgDialogComponent.setContentHeight(contentHeight);
    }
  }

 Function dimensionIsValid()

 /**
 * Simple function to check that sensible numeric values are 
 * being proposed for a dimension
 * @param sampleDimension 
 * @return booolean
 */
function dimensionIsValid(sampleDimension){
    return (!isNaN(sampleDimension) && sampleDimension > 0);
}

Function  initializeDefaultMessageDialogSize()

 /**
 * This function will re-define the default sizing applied by the framework 
 * in 11.1.2.n versions
 * It is designed to be called with the document onLoad event
 */
function initializeDefaultMessageDialogSize(loadEvent){
  //get the configuration information
  var documentId = loadEvent.getSource().getProperty('documentId');
  var newWidth = loadEvent.getSource().getProperty('defaultMessageDialogContentWidth');
  var newHeight = loadEvent.getSource().getProperty('defaultMessageDialogContentHeight');
  resizeDefaultMessageDialog(documentId, newWidth, newHeight);
}

Wiring in the Functions

As usual, the first thing we need to do when using JavaScript with ADF is to define an af:resource  in the document metaContainer facet

<af:document>

   ....  

   <f:facet name="metaContainer">
     <af:resource type="javascript" source="/resources/js/hackMessagedDialog.js"/>
   </f:facet>
</af:document>

This makes the script functions available to call. 

Next if you want to use the option of defining an initial default size for the dialog you use a combination of <af:clientListener> and <af:clientAttribute> tags like this.

<af:document title="MyApp" id="doc1">
   <af:clientListener method="initializeDefaultMessageDialogSize" type="load"/>
   <af:clientAttribute name="documentId" value="doc1"/>
   <af:clientAttribute name="defaultMessageDialogContentWidth" value="400"/>
   <af:clientAttribute name="defaultMessageDialogContentHeight" value="150"/> 
... 

 Just in Time Dialog Sizing 

So  what happens if you have a variety of messages that you might add and in some cases you need a small dialog and an other cases a large one? Well in that case you can re-size these dialogs just before you submit the message. Here's some example Java code:

FacesContext ctx = FacesContext.getCurrentInstance();
        
//reset the default dialog size for this message
ExtendedRenderKitService service = 
             Service.getRenderKitService(ctx, ExtendedRenderKitService.class);
service.addScript(ctx, "resizeDefaultMessageDialog('doc1',100,50);");
        
FacesMessage msg = new FacesMessage("Short message");
msg.setSeverity(FacesMessage.SEVERITY_ERROR);
ctx.addMessage(null, msg); 

So there you have it. This technique should, at least, allow you to control the dialog sizing just enough to stop really objectionable whitespace or scrollbars.

1 Don't worry if you don't get the reference, lest's just say my kids watch too many adverts.

Saturday Jul 14, 2012

Auto-Reduce Search Sample

For a while now I've been playing with techniques in ADF applications which will produce a user experience that is more webby (if I can use that term) and less business application like.  Some of this work can be seen if you look back on my postings on animation.

A recent challenge, in this vein, from one of the internal teams within Oracle, was to provide a search facility that would auto-reduce the results list in situe without the user having to press a button or link to trigger the search.

Now hopefully you're familiar with the component behaviour <af:autoSuggestBehavior> that will provide a drop down list below a test input that revises as you type. For example:

autosuggestBehavior in action

But that's not what I wanted here. In this case, I wanted the main search results to update as I typed - a feature you will find on certain popular search engines. As part of the process of putting this together, I found myself combining code and patterns from other prototypes that I've worked on and came to the conclusion that I could wrap all of this into a nice little demo application that actually shows several interesting techniques and patterns as well as the aforementioned auto-reduce.

Here's the screen, it's a simple search against ALL_OBJECTS in the database, and the features of it that I thought were interesting. 

Screen Shot of sample application in row view

  1. The screen uses a fixed width centered display area, a fairly popular layout pattern for a lot of sites, including this blog.
  2. The images displayed by each row use a technique called the ImageMap Pattern to derive the correct image to display. I'll be talking about two variants on this pattern, the more interesting one of which using image sprites as a way to reduce your network traffic.
  3. Typing in the search field will (after a configurable delay) cause the query to be re-executed and a revised list displayed.
  4. We have a dynamic record count which shows the records shown out of the total. 
  5. The list view here has a smart pagination bar which allows the user access to the start and end of the list without printing out every option in between.
  6. You can change how many records are displayed and hence the size of the pagination.
  7. Finally you can switch between row and icon views. This latter function is interesting because it's carried out client side to minimize the switch time.

The results of typing into the search screen would look something like this:

AutoReduce results

 As you type, not only will the list reduce, but of course the pagination bar etc. will be updated to reflect the current result set size.

The icon view mentioned in (7) looks like this:

Icon View

Over the next couple of weeks I'll be writing detailed articles on these various features, but if you can't wait to get started, you can download the sample from the ADF Samples project on Java.net: DRM004 - AutoReduce and Pagination screen

Sunday Jun 24, 2012

Making Those PanelBoxes Behave

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.

  1. I start the page with all panelBoxes collapsed, all set by the EL state I'm storing on the session
  2. I manually disclose panelBox no 1.
  3. 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.
  4. 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

Saturday May 08, 2010

Script For Detecting Availability of XMLHttp in Internet Explorer

Having the XMLHttpRequest API available is key to any ADF Faces Rich Client application. Unfortunately, it is possible for users to switch off this option in Internet Explorer as a Security setting. Without XMLHttpRequest available, your ADF Faces application will simply not work correctly, but rather than giving the user a bad user experience wouldn't it be nicer to tell them that they need to make some changes in order to use the application? 
Thanks to Blake Sullivan in the ADF Faces team we now have a little script that can do just this.
The script is available from the sample code page here, just look for the browserCheck.js sample - This is what you'll need to add to your project.
The best way to use this script is to make changes to whatever template you are using for the entry points to your application. If you're not currently using template then you'll have to make the same change in each of your JSPX pages.
  1. Save the browserCheck.js file into a /js/ directory under your HTML root within your UI project (e.g. ViewController)
  2. In the template or page, select the <af:document> object in the Structure window.
  3. From the right mouse (context) menu choose Facet and select the metaContainer facet.
  4. Switch to the source code view and locate the metaContainer facet. Then insert the following lines (I've included the facet tag for clarity but you'll already have that):
      <f:facet name="metaContainer">
        <af:resource type="javascript"
                     source="/js/browserCheck.js"/>
        <af:resource type="javascript">
           xmlhttpNativeCheck(
                     "help/howToConfigureYourBrowser.html");
        </af:resource>
      </f:facet>

Note that the argument to the xmlhttpNativeCheck function is a page that you want to show to the user if they need to change their browser configuration. So build this page in the appropriate place as well. You can also just call the function without any arguments e.g. xmlhttpNativeCheck(); in which case it will pop up default instructions for the user to follow, but not redirect to any other page.
About

Hawaii, Yes! Duncan has been around Oracle technology way too long but occasionally has interesting things to say. He works in the Development Tools Division at Oracle, but you guessed that right? In his spare time he contributes to the Hudson CI Server Project at Eclipse
Follow DuncanMills on Twitter

Note that comments on this blog are moderated so (1) There may be a delay before it gets published (2) I reserve the right to ignore silly questions and comment spam is not tolerated - it gets deleted so don't even bother, we all have better things to do with our lives.
However, don't be put off, I want to hear what you have to say!

Search

Archives
« April 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today