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

Tuesday Apr 02, 2013

panelGridLayout - now we are complete

Good news, with the arrival of 11.1.1.7 (Patchset 6) the immensely useful <af:panelGridLayout> component has made it into the 11.1.1.n code-line.

If you're not familiar with panelGridLayout then check out my article on the subject from earlier and then go and check out the demo page which will link you off to the documentation etc. 

Thursday Feb 21, 2013

Get More From Your Messages

Within ADF Faces we take much goodness and added value for granted.  One such feature came to my attention just the other day. Had you noticed that when you have several errors outstanding on the screen the framework gives you a hyperlink to set focus to that field? See the error on commissionPct below and the hyperlink that you could click to move focus to it in the screen:

 So that's neat and I must confess that although I must have stared this one in the face hundreds of times I never really groked how cool that was. 

Anyway, as is usual, this was not just a random act of attention on my part but rather considering a couple of different user cases which both boiled down to the basic question.  Can we extend these error dialogs in some way to show more detail or carry out some other action in response to the error? So that's what I wanted to work through in this article, because the answer is (of course) yes!

How to make the Messages Interactive?

So let's take a look at how to do this. The clue is in the documentation for the tag which casually mentions that  you can include HTML into your Faces messages or message detail. and one of the examples is the embedding of an anchor tag to embed a hyperlink - something that could indeed be useful in a message.  So it occurred to me that this hyperlink provided the opportunity for  an actionable gesture that the user could make to do stuff within the actual application as well as jump off to another one. However, it's not all plain sailing, you can't just embed any old HTML markup into the messages (I did try!) So no embedding of buttons, and even for an <a> tag you can't wire in JavaScript with an onClick=, that all get stripped out.

However, with a carefully shaped href attribute you can call JavaScript. So for example I've crafted the following message detail string:

<html>Detail for errror <a href=javascript:errorDetailCallback('it1','d1');>here</a></html>

 This renders the word "here" as a link in the message and then when the user clicks on that it calls a JavaScript errorDetailCallback() with a couple of arguments.  This function is included in a script linked onto the page using a standard <af:resource> tag.

The script itself could do anything. In my sample I'm actually making it call back to the server (via an <af:serverListener> defined in the document. Here's the JavaScript function:

 function errorDetailCallback(errorContext, documentComponentId){
    
    var callbackComponent = AdfPage.PAGE.findComponentByAbsoluteId(documentComponentId);
    if (callbackComponent != null) {
        var serverEvent = new AdfCustomEvent(callbackComponent, 
                                             "errorDetailEvent",
                                             {detailKey : errorContext},
                                             true);
        serverEvent.queue(true);
    }
    else{
        AdfLogger.LOGGER.severe("errorDetailCallback: Error unable to locate document component: " 
                                + documentComponentId);
    }  
}

The custom event is wired into the server via the <af:serverListener> tag:

<af:serverListener type="errorDetailEvent" 
                   method="#{indexPageBB.errorDrilldownHandler}"/>

 The server side method (errorDrilldownHandler()) in my case gets the custom event and then shows a popup in response:

 public void errorDrilldownHandler(ClientEvent clientEvent) {
   Map params = clientEvent.getParameters();
   String detailKey = (String)params.get("detailKey");
   if (detailKey != null && _errorDetail.containsKey(detailKey)){
     _lastErrorKey = detailKey;
   }
   else{
     _lastErrorKey = "UNKNOWN";
   }
        
   //Now do what you want. In this case show a popup with a detail message
   RichPopup.PopupHints hints = new RichPopup.PopupHints();
   getDetailPopup().show(hints);
 }

Note that in the above case, the _lastErrorKey  is simply a variable which is used by the getter that populates the text in the popup, it's used as a key into a Map with some further message details in it. However, what you do in this callback routine is of course up to you. 

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.

Thursday Oct 25, 2012

Centered Content using panelGridLayout

A classic layout conundrum,  which I think pretty much every ADF developer may have faced at some time or other, is that of truly centered (centred) layout. Typically this requirement comes up in relation to say displaying a login type screen or similar.

Superficially the  problem seems easy, but as my buddy Eduardo explained when discussing this subject a couple of years ago it's actually a little more complex than you might have thought. If fact, even the "solution" provided in that posting is not perfect and suffers from a several issues (not Eduardo's fault, just limitations of panelStretch!)

  1. The top, bottom, end and start facets all need something in them
  2. The percentages you apply to the topHeight, startWidth etc. are calculated as part of the whole width.  This means that you have to guestimate the correct percentage based on your typical screen size and the sizing of the centered content. So, at best, you will in fact only get approximate centering, and the more you tune that centering for a particular browser size the more it will fail if the user resizes.
  3. You can't attach styles to the panelStretchLayout facets so to provide things like background color or fixed sizing you need to embed another container that you can apply styles to, typically a panelgroupLayout

 

For reference here's the code to print a simple 100px x 100px red centered square  using the panelStretchLayout solution, approximately tuned to a 1980 x 1080 maximized browser (IDs omitted for brevity):

<af:panelStretchLayout startWidth="45%" endWidth="45%" 
                       topHeight="45%"  bottomHeight="45%" >
  <f:facet name="center">
    <af:panelGroupLayout inlineStyle="height:100px;width:100px;background-color:red;" 
                         layout="vertical"/>
  </f:facet>
  <f:facet name="top">
    <af:spacer height="1" width="1"/>
  </f:facet>
  <f:facet name="bottom">
    <af:spacer height="1" width="1"/>
  </f:facet>
  <f:facet name="start">
    <af:spacer height="1" width="1"/>
  </f:facet>
  <f:facet name="end">
    <af:spacer height="1" width="1"/>
   </f:facet>
</af:panelStretchLayout> 

And so to panelGridLayout 

So here's the  good news, panelGridLayout makes this really easy and it works without the caveats above.  The key point is that percentages used in the grid definition are evaluated after the fixed sizes are taken into account, so rather than having to guestimate what percentage will "more, or less", center the content you can just say "allocate half of what's left" to the flexible content and you're done.

Here's the same example using panelGridLayout:

<af:panelGridLayout>
  <af:gridRow height="50%"/>
  <af:gridRow height="100px">
    <af:gridCell width="50%" />
    <af:gridCell width="100px" halign="stretch" valign="stretch" 
                 inlineStyle="background-color:red;">
      <af:spacer width="1" height="1"/>
    </af:gridCell>
    <af:gridCell width="50%" />
  </af:gridRow>
  <af:gridRow height="50%"/>
</af:panelGridLayout>

 So you can see that the amount of markup is somewhat smaller (as is, I should mention, the generated DOM structure in the browser), mainly because we don't need to introduce artificial components to ensure that facets are actually observed in the final result.  But the key thing here is that the centering is no longer approximate and it will work as expected as the user resizes the browser screen.  By far this is a more satisfactory solution and although it's only a simple example, it will hopefully open your eyes to the potential of panelGridLayout as your number one, go-to layout container.

Just a reminder though, right now, panelGridLayout is only available in 11.1.2.2 and above.

Thursday Sep 27, 2012

PanelGridLayout - A Layout Revolution

With the most recent 11.1.2 patchset (11.1.2.3) there has been a lot of excitement around ADF Essentials (and rightly so), however, in all the fuss I didn't want an even more significant change to get missed - yes you read that correctly, a more significant change! I'm talking about the new panelGridLayout component, I can confidently say that this one of the most revolutionary components that we've introduced in 11g, even though it sounds rather boring. To be totally accurate, panelGrid was introduced in 11.1.2.2 but without any presence in the component palette or other design time support, so it was largely missed unless you read the release notes. However in this latest patchset it's finally front and center. Its time to explore - we (really) need to talk about layout. 

Let's face it,with ADF Faces rich client, layout is a rather arcane pursuit, once you are a layout master, all bow before you, but it's more of an art than a science, and it is often, in fact, way too difficult to achieve what should (apparently) be a pretty simple.

Here's a great example, it's a homework assignment I set for folks I'm teaching this stuff to: 

Sample Layout

The requirements for this layout are:

  1. The header is 80px high, the footer is 30px. These are both fixed. 
  2. The first section of the header containing the logo is 180px wide
  3. The logo is centered within the top left hand corner of the header 
  4. The title text is start aligned in the center zone of the header and will wrap if the browser window is narrowed. It should be aligned in the center of the vertical space 
  5. The about link is anchored to the right hand side of the browser with a 20px gap and again is center aligned vertically. It will move as the browser window is reduced in width.
  6. The footer has a right aligned copyright statement, again middle aligned within a 30px high footer region and with a 20px buffer to the right hand edge. It will move as the browser window is reduced in width.
  7. All remaining space is given to a central zone, which, in this case contains a panelSplitter.
  8. Expect that at some point in time you'll need a separate messages line in the center of the footer. 

In the homework assigment I set I also stipulate that no inlineStyles can be used to control alignment or margins and no use of other taglibs (e.g. JSF HTML or Trinidad HTML). 
So, if we take this purist approach, that basic page layout (in my stock solution) requires 3 panelStretchLayouts, 5 panelGroupLayouts and 4 spacers - not including the spacer I use for the logo and the contents of the central zone splitter - phew! The point is that even a seemingly simple layout needs a bit of thinking about, particulatly when you consider strechting and browser re-size behavior. In fact, this little sample actually teaches you much of what you need to know to become vaguely competant at layouts in the framework. The underlying result of "the way things are" is that most of us reach for panelStretchLayout before even finishing the first sip of coffee as we embark on a new page design. In fact most pages you will see in any moderately complex ADF page will basically be nested panelStretchLayouts and panelGroupLayouts, sometimes many, many levels deep. 
So this is a problem, we've known this for some time and now we have a good solution. (I should point out that the oft-used Trinidad trh tags are not a particularly good solution as you're tie-ing yourself to an HTML table based layout in that case with a host of attendent issues in resize and bi-di behavior, but I digress.)


So, tadaaa, I give to you panelGridLayout. PanelGrid, as the name suggests takes a grid like (dare I say slightly gridbag-like) approach to layout, dividing your layout into rows and colums with margins, sizing, stretch behaviour, colspans and rowspans all rolled in, all without the use of inlineStyle. As such, it provides for a much more powerful and consise way of defining a layout such as the one above that is actually simpler and much more logical to design. The basic building blocks are the panelGridLayout itself, gridRow and gridCell. Your content sits inside the cells inside the rows, all helpfully allowing both streching, valign and halign definitions without the need to nest further panelGroupLayouts. So much simpler! 


If I break down the homework example above my nested comglomorate of 12 containers and spacers can be condensed down into a single panelGrid with 3 rows and 5 cell definitions (39 lines of source reduced to 24 in the case of the sample). What's more, the actual runtime representation in the browser DOM is much, much simpler, and clean, with basically one DIV per cell (Note that just because the panelGridLayout semantics looks like an HTML table does not mean that it's rendered that way!) .


Another hidden benefit is the runtime cost. Because we can use a single layout to achieve much more complex geometries the client side layout code inside the browser is having to work a lot less. This will be a real benefit if your application needs to run on lower powered clients such as netbooks or tablets.


So, it's time, if you're on 11.1.2.2 or above, to smile warmly at your panelStretchLayouts, wrap the blanket around it's knees and wheel it off to the Sunset Retirement Home for a well deserved rest. There's a new kid on the block and it wants to be your friend. 

Update: panelGridLayout is also available in the 11.1.1.7 release as well as the 11.1.2.n series. 

Friday Jul 20, 2012

The ImageMap Pattern

In my last article, I alluded to the fact that the associated sample combined a bunch of existing patterns and techniques, and that I would progressively write those up.  In this article I'm going to talk about the first of these, the ImageMap pattern.

What's the Use Case?

 This pattern is all about solving a pretty common problem, I have, in my service model, some kind of codified value (e.g. a Type or a Status) which I want to reflect visually in my UI using an image. Now if you only have a couple of options to represent then you can use a simple ternary expression in the page, switching based on the value you're getting from the service.  However, once you get over three or four options the EL starts to get hard to read and maintain, and face it, sometimes you might not even notice that it's evaluating to the wrong thing if the expression is wrong.  So what are the options for approaching this? 

  1. Do the translation in the service layer and provide an attribute containing the correct image to display. 
  2. Manage some simple lookup into a shared UI level resource

Option (1) just feels wrong because you'd be letting client side information, the names of image files in this case,  leak into the wrong layer, so the shared resource approach really looks like the way to go. 

Implementation

The approach that we take in this pattern is to exploit the ability of Expression Language to be able to refer to both arrays and maps using the "[<value>]" syntax. For example the expression mybean.image[1] can actually mean several things depending on what "image" is in this case. If the getImage() method in the underlying mybean returns a List then this would translate to pull out index 1 from that list. If on the other hand getImage() returns a Map then the get() method will be called on the map with a key of (in this case) "1".


We can exploit this behavior, and particularly the understanding of the Map expressions to define a mapping between a piece of data from the model, such as a status code, and a particular image to use to represent that. To illustrate this let's take a simple example where we have some possible string status values in the datamodel and want to map that into different images, thus:

Code from the Service Image To Use
 TABLE  /images/table.png
 VIEW  /images/view.png
FUNCTION  /images/plsql_func.png
 ...  ..

Notice that the names of the images are not quite the same as the codification from the service layer so we can't get away with the simplier solution of:

<af:image source="/images/#{bindings.ObjectType}.png" .../> 

So instead, we have to work via an abstraction using a lookup map for the images 

Step 1: Define your image map

We can define the lookup map to work from either as an explicit managed bean, or even more easily as a stand-alone bean definition in the adfc-config.xml file. Conventionally we will store this bean on the application scope so that all users on the system share the same copy:

<managed-bean>
   <managed-bean-name>typeImages</managed-bean-name>
   <managed-bean-class>java.util.HashMap</managed-bean-class>
   <managed-bean-scope>application</managed-bean-scope>
   <map-entries>
     <map-entry>
       <key>TABLE</key>
       <value>/images/table.png</value>
     </map-entry>
     <map-entry>
       <key>VIEW</key>
       <value>/images/view.png</value>
     </map-entry>
     <map-entry>
       <key>FUNCTION</key>
       <value>/images/pls_func.png</value>
     </map-entry>
     ... 
   </map-entries>
</managed-bean>  

Alternatively if the thing we wanted to key off was a simple numerical list then we could have a definition that used a simple array rather than a map; like this:

<managed-bean>
  <managed-bean-name>lifecycleImages</managed-bean-name>
  <managed-bean-class>java.util.ArrayList</managed-bean-class>
  <managed-bean-scope>application</managed-bean-scope>
  <list-entries>
    <value-class>java.lang.String</value-class>
    <value>/images/new.png</value>
    <value>/images/updated.png</value>
    <value>/images/sclosed.png</value>
  </list-entries>
</managed-bean>  

In this latter case index lifecycleImages[0] would map to /images/new.png, lifecycleImages[1] to /images/updated.png etc.

Step 2: using this bean in EL

Now that the list or map has been defined we can use it thus:

<af:image source="#{typeImages[bindings.ObjectType]}"

Where the bindings.ObjectType attribute binding can be expected to turn one of the valid keys in the map (TABLE,VIEW etc.)

 You can see an example of this version of the pattern inside the tile iterator in the home.jspx page in the sample: DRM004 - AutoReduce and Pagination screen

Variations on the Theme

 I'm always thinking about how to optimize performance, and one useful approach when it comes to images is to use the technique of using image sprites. This is where, instead of having lots of discrete images, you have a single image file, a little like a film-strip, containing all of the images. In this case, CSS is used to assign the filmstrip image as a background to a page element and then the CSS background-position is set to select a particular icon on the strip. This has the advantage of only needing a single round trip to grab all the images at once, having a positive effect on your page load time. 

Using this idea we can take the Image Map Pattern approach, but rather than having the map entry value be the name of a discrete image, it can point to the name of the CSS style. This style can be applied in the styleClass attribute of an element to pull the required image from the film-strip. To save you having to create a bunch of extra styles to encode the positions, you could also just hold an offset in the film-strip as the lookup value. 

This latter approach can also be seen in the  DRM004 - AutoReduce and Pagination screen sample, have a look at the managed bean definition for typeImageOffsets. This information is then used to define the images in the list view of the home.jspx page, thus:

<af:spacer styleClass="iconStripBase" inlineStyle="background-position:#{typeImageOffsets[row.ObjectType]}px;"/> 

 Have a look at the sample to see it all in action. 

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

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
« August 2015
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
31
      
Today