Saturday Nov 17, 2012

Towards Ultra-Reusability for ADF - Adaptive Bindings

The task flow mechanism embodies one of the key value propositions of the ADF Framework, it's primary contribution being the componentization of your applications and implicitly the introduction of a re-use culture, particularly in large applications.

However, what if we could do more? How could we make task flows even more re-usable than they are today? Well one great technique is to take advantage of a feature that is already present in the framework, a feature which I will call, for want of a better name, "adaptive bindings".

What's an adaptive binding? well consider a simple use case.  I have several screens within my application which display tabular data which are all essentially identical, the only difference is that they happen to be based on different data collections (View Objects, Bean collections, whatever) , and have a different set of columns. Apart from that, however, they happen to be identical; same toolbar, same key functions and so on. So wouldn't it be nice if I could have a single parametrized task flow to represent that type of UI and reuse it?

Hold on you say, great idea, however, to do that we'd run into problems. Each different collection that I want to display needs different entries in the pageDef file and:

  1. I want to continue to use the ADF Bindings mechanism rather than dropping back to passing the whole collection into the taskflow  
  2. If I do use bindings, there is no way I want to have to declare iterators and tree bindings for every possible collection that I might want the flow to handle

 Ah, joy! I reply, no need to panic, you can just use adaptive bindings.

Defining an Adaptive Binding 

It's easiest to explain with a simple before and after use case.  Here's a basic pageDef definition for our familiar Departments table. 

<executables>
  <iterator Binds="DepartmentsView1" 
            DataControl="HRAppModuleDataControl"
            RangeSize="25" 
            id="DepartmentsView1Iterator"/>
</executables>
<bindings>
  <tree IterBinding="DepartmentsView1Iterator" id="DepartmentsView1">
    <nodeDefinition DefName="oracle.demo.model.vo.DepartmentsView" Name="DepartmentsView10">
      <AttrNames>
        <Item Value="DepartmentId"/>
        <Item Value="DepartmentName"/>
        <Item Value="ManagerId"/>
        <Item Value="LocationId"/>
      </AttrNames>
    </nodeDefinition>
  </tree>
</bindings> 

Here's the adaptive version:

<executables>
  <iterator Binds="${pageFlowScope.voName}" 
            DataControl="HRAppModuleDataControl"
            RangeSize="25" 
            id="TableSourceIterator"/>
</executables>
<bindings>
  <tree IterBinding="TableSourceIterator" id="GenericView">
      <nodeDefinition Name="GenericViewNode"/>
  </tree>
</bindings> 

You'll notice three changes here.  

  1. Most importantly, you'll see that the hard-coded View Object name  that formally populated the iterator Binds attribute is gone and has been replaced by an expression (${pageFlowScope.voName}). This of course, is key, you can see that we can pass a parameter to the task flow, telling it exactly what VO to instantiate to populate this table!
  2. I've changed the IDs of the iterator and the tree binding, simply to reflect that they are now re-usable
  3. The tree binding itself has simplified and the node definition is now empty.  Now what this effectively means is that the #{node} map exposed through the tree binding will expose every attribute of the underlying iterator's collection - neat! (kudos to Eugene Fedorenko at this point who reminded me that this was even possible in his excellent "deep dive" session at OpenWorld  this year)

Using the adaptive binding in the UI

Now we have a parametrized  binding we have to make changes in the UI as well, first of all to reflect the new ID that we've assigned to the binding (of course) but also to change the column list from being a fixed known list to being a generic metadata driven set:

<af:table value="#{bindings.GenericView.collectionModel}"
          rows="#{bindings.GenericView.rangeSize}"
          fetchSize="#{bindings.GenericView.rangeSize}"
          emptyText="#{bindings.GenericView.viewable ? 'No data to display.' : 'Access Denied.'}"
          var="row" rowBandingInterval="0" 
          selectedRowKeys="#{bindings.GenericView.collectionModel.selectedRow}"
          selectionListener="#{bindings.GenericView.collectionModel.makeCurrent}"
          rowSelection="single" id="t1">
  <af:forEach items="#{bindings.GenericView.attributeDefs}" var="def">
    <af:column headerText="#{bindings.GenericView.labels[def.name]}" sortable="true"
               sortProperty="#{def.name}" id="c1">
      <af:outputText value="#{row.bindings[def.name].inputValue}" id="ot1"/>
    </af:column>
  </af:forEach>
</af:table>

Of course you are not constrained to a simple read only table here.  It's a normal tree binding and iterator that you are using behind the scenes so you can do all the usual things, but you can see the value of using ADFBC as the back end model as you have the rich pantheon of UI hints to use to derive things like labels (and validators and converters...) 

One Final Twist

 To finish on a high note I wanted to point out that you can take this even further and achieve the ultra-reusability I promised. Here's the new version of the pageDef iterator, see if you can notice the subtle change?

<iterator Binds="{pageFlowScope.voName}" 
          DataControl="${pageFlowScope.dataControlName}"
          RangeSize="25" 
          id="TableSourceIterator"/> 

Yes, as well as parametrizing the collection (VO) name, we can also parametrize the name of the data control. So your task flow can graduate from being re-usable within an application to being truly generic. So if you have some really common patterns within your app you can wrap them up and reuse then across multiple developments without having to dictate data control names, or connection names. This also demonstrates the importance of interacting with data only via the binding layer APIs. If you keep any code in the task flow generic in that way you can deal with data from multiple types of data controls, not just one flavour. Enjoy!

Update

Read this post as well on overcoming possible refresh problems when changing the source on a single page. 

Further update

Check out this article from Luc Bors on using similar ideas with Query Components / View Criteria.  

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.

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
« November 2012 »
MonTueWedThuFriSatSun
   
1
2
3
4
5
6
7
8
9
10
11
12
13
15
16
18
19
20
21
22
23
24
25
26
27
28
29
30
  
       
Today