Wednesday 2 September 2009

JDev 11gR1 - af:tree mashup – using hierarchical tables and drag n drop

This blog post demonstrates creating an ADF Faces RC af:tree component that sources its data from a hierarchical database table, and supports drag n drop of the nodes.

Both these topics have been discussed and demonstrated by other excellent bloggers, the core of this post is to bring both concepts together, with my usual own proof of concept documentation that may be useful to readers.

The ADF Faces RC support for hierarchical data sources was defined by Chandu Bhavsar.

The drag and drop support for af:trees was documented by Luc Bors.

(Definitely the kudos for this post must go to both Chandu and Luc for their posts that sparked the inspiration for mine)

Data model

In this example we'll use the following table:
CREATE TABLE organisations
(org_id NUMBER(4,0) NOT NULL
,parent_org_id NUMBER(4,0)
,name VARCHAR2(35) NOT NULL
,CONSTRAINT org_pk PRIMARY KEY (org_id)
,CONSTRAINT org_parent_fk
FOREIGN KEY (parent_org_id)
REFERENCES organisations (org_id));
Note the FK between the table and itself, essentially modelling that an organisation has sub-organisations (or agencies).

The data:
ORG_ID  PARENT_ORG_ID  NAME
------ ------------- -------------------------------
1000 (null) Sage Computing Services
1010 1000 Training Division
1030 1010 Self Study Program
1040 1010 Classroom Training
2264 (null) Australian Medical Systems
3210 (null) Conservation Society
3214 3210 Forests Division
3216 3210 Rivers Division
4394 (null) Newface cosmetics
3842 (null) Institute of Business Services
3843 3842 Marketing Services
3844 3842 Financial Services
Hierarchical af:tree

This details the steps to setup an ADF BC layer and ADF Faces RC bindings to support the af:tree with hierachical data from the organisations table, as previously described in Chandu Bhavsar post.

Note the following steps are well documented on Chandu's post, though you will find slightly more detail on creating the correct bindings below:

1) Create a Fusion Web App with an ADF BC Model project and ADF Faces RC ViewController.

Model project

2) In the Model project create an empty Application Module (AM)

3) Create an Entity Object (EO) based on your organisations table in the database.

4) Create two View Objects (VO) based off the same EM. Name the first ParentOrgView and the second LeafOrgView.

5) Modify the ParentOrgView query as follows:
SELECT Organisations.ORG_ID, 
Organisations.PARENT_ORG_ID,
Organisations.NAME
FROM ORGANISATIONS Organisations
WHERE Organisations.PARENT_ORG_ID IS NULL
6) You don't need to modify the LeafOrgView query. For completeness it's described as follows:
SELECT Organisations.ORG_ID, 
Organisations.PARENT_ORG_ID,
Organisations.NAME
FROM ORGANISATIONS Organisations
7) Create a VO Link between the ParentOrgView and LeafOrgView as follows:

Essentially:

ParentOrgView.OrgId = LeafOrgView.ParentOrgId

8) Create a second VO Link between the LeafOrgView to itself as follows:

Essentially:

LeafOrgView (source).OrgId = LeafOrgView (dest).ParentOrgId

9) Externalize the VOs through the AM using the following model:

Essentially:

ParentOrgView1
- LeafOrgView1
- - LeafOrgView2

ViewController project

10) In your ViewController project create a new blank JSF page called treeMashupDemo.jspx.

11) From the Data Control Palette drag the ParentOrgView onto the page as a tree. In the Edit Tree Binding you should see the following:

Note the first level of the tree has been defined as model.ParentOrgView.

12) For clarity when testing later, ensure both the OrgId and Name are included in the Display Attributes.

13) With the model.ParentOrgView node selected in the Tree Level Rules, select the green plus (+) button and select LeafOrgView:


14) With the LeafOrgView node selected in the Tree Level Rules, ensure both the OrgId and Name are included in the Display Attributes.

15) Again with the LeafOrgView node selected in the Tree Level Rules, again select the green plus (+) button, and select LeafOrgView__2:

Note how the Tree Level Rules only has 2 nodes, but to the right of each node is the child of the current node. Of specific interest in the hierarchical relationship between LeafOrgView and LeafOrgView__2.

16) For reference the JSF af:tree code is as follows:
<af:tree value="#{bindings.ParentOrgView1.treeModel}"
var="node"
selectionListener="#{bindings.ParentOrgView1.treeModel.makeCurrent}"
rowSelection="single"
id="t1">
<f:facet name="nodeStamp">
<af:outputText value="#{node}" id="ot1"/>
</f:facet>
</af:tree>
...the bindings look as follows:

..and the page def XML file as follows:
<?xml version="1.0" encoding="UTF-8" ?>
<pageDefinition xmlns="http://xmlns.oracle.com/adfm/uimodel" version="11.1.1.54.7" id="treeMashupDemoPageDef"
Package="view.pageDefs">
<parameters/>
<executables>
<variableIterator id="variables"/>
<iterator Binds="ParentOrgView1" RangeSize="25" DataControl="AppModuleDataControl" id="ParentOrgView1Iterator"/>
</executables>
<bindings>
<tree IterBinding="ParentOrgView1Iterator" id="ParentOrgView1">
<nodeDefinition DefName="model.ParentOrgView" Name="ParentOrgView10">
<AttrNames>
<Item Value="OrgId"/>
<Item Value="Name"/>
</AttrNames>
<Accessors>
<Item Value="LeafOrgView"/>
</Accessors>
</nodeDefinition>
<nodeDefinition DefName="model.LeafOrgView" Name="ParentOrgView11">
<AttrNames>
<Item Value="OrgId"/>
<Item Value="Name"/>
</AttrNames>
<Accessors>
<Item Value="LeafOrgView_2"/>
</Accessors>
</nodeDefinition>
</tree>
</bindings>
</pageDefinition>

Testing

On running our web page we see the following:

Note that the tree is happily showing the hierarchical data based on our tree bindings.

Adding drag n drop to the tree

The next set of functionality we wish to add is the ability to drag and drop nodes, or in our case organisations, in the tree. This involves adding support for drag n drop to the tree, as well as behind the scenes updating the dropped organisation's parent_org_id to that of the org_id of the organisation the original was dropped on.

The inspiration of this section comes from Luc Bors blog post, with slight difference in mine some of the supporting code is further spelt out:

1) As a reminder at the moment our af:tree code looks as follows:
<af:tree value="#{bindings.ParentOrgView1.treeModel}"
var="node"
selectionListener="#{bindings.ParentOrgView1.treeModel.makeCurrent}"
rowSelection="single"
id="t1">
<f:facet name="nodeStamp">
<af:outputText value="#{node}" id="ot1"/>
</f:facet>
</af:tree>
2) We then introduce a collectionDragSource and a collectionDropTarget to support the drag and drop within the tree:
<af:tree value="#{bindings.ParentOrgView1.treeModel}"
var="node"
selectionListener="#{bindings.ParentOrgView1.treeModel.makeCurrent}"
rowSelection="single"
id="t1">
<af:collectionDragSource actions="MOVE"
modelName="DnDOrganisations"/>
<af:collectionDropTarget actions="MOVE"
modelName="DnDOrganisations"
dropListener="#{treeBean.dragAndDrop}"/>
<f:facet name="nodeStamp">
<af:outputText value="#{node}" id="ot1"/>
</f:facet>
</af:tree>
Note both support the MOVE action, they both specify a matching modelName DnDOrganisations, and finally the dropListener maps to a backing bean method we'll define in a moment. It's essential the modelName's match including the case of the name, and we'll be referring to this in the dragAndDrop backing bean treeBean method in a moment.

3) In your adfc-config.xml file declare a bean treeBean of class view.TreeBean:


4) And create a Java class TreeBean in your view package within the ViewController project.
package view;

public class TreeBean {

}
The real work for drag and drop is done in the backing bean method. However in order for the code to work there are a couple of items we need to configure in the Model project:

5) Create an AppModuleImpl for the AM

6) Create a LeafOrgViewImpl for the VO

7) Create a LeafOrgViewRowImpl for the VO and ensure to include the accessors

8) In the AM expose the LeafOrgView VO one more time as its own node (not a child), and call the usage LeafOrgViewAll as follows:


9) Finally the dragAndDrop code is as follows. Note the code includes inline documentation comments that explains what is occurring:
package view;

import model.AppModuleImpl;
import model.LeafOrgViewImpl;
import model.LeafOrgViewRowImpl;
import model.ParentOrgViewRowImpl;
import oracle.adf.model.BindingContext;
import oracle.adf.view.rich.component.rich.data.RichTree;
import oracle.adf.view.rich.datatransfer.DataFlavor;
import oracle.adf.view.rich.datatransfer.Transferable;
import oracle.adf.view.rich.dnd.DnDAction;
import oracle.adf.view.rich.event.DropEvent;
import oracle.adfinternal.view.faces.model.binding.FacesCtrlHierNodeBinding;
import oracle.jbo.Key;
import oracle.jbo.uicli.binding.JUCtrlHierNodeBinding;
import org.apache.myfaces.trinidad.model.CollectionModel;
import org.apache.myfaces.trinidad.model.RowKeySet;

public class TreeBean {

public DnDAction dragAndDrop(DropEvent dropEvent) {
// The default action - do nothing
DnDAction result = DnDAction.NONE;

// Represents the object that was dropped
Transferable draggedTransferObject = dropEvent.getTransferable();

// The data in the draggedTransferObject "Transferrable" object is the row key for the dragged component.
// Note how the DnDOrganisations value in the call to getDataFlavor() matches the collectionDragSource
// and collectionDropTarget tags model attributes in our page. It's essential the strings exactly match.
DataFlavor<RowKeySet> draggedRowKeySetFlavor = DataFlavor.getDataFlavor(RowKeySet.class, "DnDOrganisations");
RowKeySet draggedRowKeySet = draggedTransferObject.getData(draggedRowKeySetFlavor);

if (draggedRowKeySet != null) {
// We grab the tree's data model, essentially the CollectionModel that stores the complete tree of nodes
CollectionModel treeModel = draggedTransferObject.getData(CollectionModel.class);

// Ask the collection model to set the current row/node to that of the transferrable object that was dropped
Object draggedKey = draggedRowKeySet.iterator().next();
treeModel.setRowKey(draggedKey);

// Grab that current row (thanks to the last statements work) and get the row's OrgId. It's essential the
// OrgId is one of the displayed attributes in the tree binding.
FacesCtrlHierNodeBinding draggedTreeNode = (FacesCtrlHierNodeBinding)treeModel.getRowData();
oracle.jbo.domain.Number draggedTreeNodeId = (oracle.jbo.domain.Number)draggedTreeNode.getAttribute("OrgId");

// The dropEvent carries the target/location's row key where the dropped organisations was dropped
Object serverRowKey = dropEvent.getDropSite();
RichTree richTree = (RichTree)dropEvent.getDropComponent();
// This time we use the tree itself to make it's current row that of the server row key (ie. the destination)
richTree.setRowKey(serverRowKey);
// And we retrieve that row's index
int rowIndex = richTree.getRowIndex();

// The rich tree based on the index allows us to retrieve that current row/organisation's OrgId
oracle.jbo.domain.Number targetNodeId = (oracle.jbo.domain.Number)((JUCtrlHierNodeBinding)richTree.getRowData(rowIndex)).getAttribute("OrgId");

// At this point we now have the OrgId of the dropped organisation (draggedTreeNodeId) and the OrgId of the
// organisation that is the target. From here we simply want to update the dropped organisations' ParentOrgId
// to the OrgId of the target. This is best done through the model layer.
//
// Normally this would be best done by fetching the appropriate iterator bindings and making the changes through
// the bindings. However in this case the tree doesn't expose any iterator for the leaf nodes, so we need to
// resort to retrieve the Model project's objects and do the work ourself.

// Retrieve the AM and then a handle on the LeafOrgViewAll - this gives us access to all rows regardless of
// where they exist in the hierarchy
AppModuleImpl am = (AppModuleImpl)BindingContext.getCurrent().getDefaultDataControl().getApplicationModule();
LeafOrgViewImpl leafOrgView = (LeafOrgViewImpl)am.getLeafOrgViewAll();

// Given the dragged organisation's OrgId, construct a key object, and then retrieve that row from the VO using
// the key
Object[] nodeObjectKey = new Object[] {draggedTreeNodeId};
Key nodeKey = new Key(nodeObjectKey);
LeafOrgViewRowImpl nodeRow = (LeafOrgViewRowImpl)leafOrgView.getRow(nodeKey);

// See below
boolean parentNode = nodeRow.getParentOrgId() == null;

// Finally update that organisaiton's ParentOrgId to that of the target organisation's OrgId
nodeRow.setParentOrgId(targetNodeId);

// And commit the changes – obviously this has side effects on any other uncommitted data, be careful
am.getDBTransaction().commit();

// If we've moved a parent node to become a leaf, we need to force the parent VO to requery itself to correctly
// reflect the data change. This is destructive on the current expand/collapsed state of the tree.
// I'm not overly sure of a solution for this; maybe a reader can suggest one.
if (parentNode) {
am.getParentOrgView1().clearCache();
am.getParentOrgView1().executeQuery();
}

// Indicate to the dragEvent that the operation was succesful and visually the move should occur in the tree
result = DnDAction.MOVE;
}
return result;
}
}
You'll note in the code I take pains to mention it's essential the OrgId is one of the displayed attributes for the tree. If you fail to supply this the routine has no OrgId attribute to fetch the OrgId value from.

This does pose a problem, because while during testing it's fine to show the OrgId and Name of the organisations in the tree, for production we might not want to show these meaningless internal ID numbers to the user. The simple fix for this is to return to the af:tree and update the af:outputText component who is responsible for what values to show for each node in the tree, changing the EL expression from #{code} to #{code.Name}:
<af:tree value="#{bindings.ParentOrgView1.treeModel}"
var="node"
selectionListener="#{bindings.ParentOrgView1.treeModel.makeCurrent}"
rowSelection="single"
id="t1">
<af:collectionDragSource actions="MOVE"
modelName="DnDOrganisations"/>
<af:collectionDropTarget actions="MOVE"
modelName="DnDOrganisations"
dropListener="#{treeBean.dragAndDrop}"/>
<f:facet name="nodeStamp">
<af:outputText value="#{node.Name}" id="ot1"/>
</f:facet>
</af:tree>
This ensures only the name attribute is displayed:

4 comments:

Chris Muir said...

(CM - The following comment passed on by Steve Muench, with my thanks to Steve)

Chris,

Few points on this article on trees.

1. You can build a recursive tree with a single view object instead of one view object for the "root" and another for the "non-root" nodes.

2. You don't need to add the view instances to the data model for the lower levels in most cases since the tree binding uses view link accessor attributes and not data model view instances for accessing child rows/nodes. In fact, adding these unnecessary actively-coordinated view link instances to the data model can be detrimental to performance since doing so is an explicitly request to issue additional DB queries when the master VO current row changes (which won't be serving any purpose in the UI).

See my Targets, Accessors, and Trees article in Oracle Magazine for some tips on these.

http://www.oracle.com/technology/oramag/oracle/09-jul/o49frame.html

(CM - I hope to follow up on Steve's article with an updated example in the next few weeks)

Chandu Bhavsar said...

Hi Chris, thx much for taking time to add upon my post. Glad to see that blog post didn't go waste! ;-)

And I guess I'll also reply to Steve's comments indirectly (by way of Chris's comment above):

Aha, so the clever trick of defining a view criteria to define "what is a root node" takes care of initiating the recursion! This was the key piece which I was missing, which "forced" me at the time to break recursion using separate VOs like that. I'll try to provide an update on my blog to point to Steve's cool example when I get time (which sadly these days is, never ;-).

I guess the fun all started with Chris's very own post on OTN little over year ago, and we've made a long trip trying various tricks ending with Steve's cool optimum solution. All is well that ends well! ;-)

P.S. In spite of all of this, I suppose there is still merit in addressing point in Chris's original OTN post: Out of the box support (in all 3 tiers - ADF BC, data binding and view) for handling SQL "START WITH .. CONNECT BY" recursion to display as a tree. One could argue the priority for that is somewhat low in light of Steve's cool solution. But that's a topic for another day.

Ludovic DESSEMON said...

Hi all, the link from chris doesn't exists, here the new link :
http://www.oracle.com/technetwork/issue-archive/2009/09-jul/o49frame-101688.html

Chris Muir said...

Thanks for the update Ludovic.

CM.