Saturday, November 5, 2011

Sub-flow Design Pattern

A design pattern is a formal documentation of a proven solution to a common problem. Within Oracle Fusion Web Applications[1], there are many design patterns embedded in their design. One of them is sub-flow design pattern.

Before you start, read this companion article first.

Usage

In sub-flow design pattern, there are two task flows involved:
  • Parent task flow (top-level)
  • Sub-flow
In Oracle Fusion Web Applications, all top-level task flows can be bookmarked and be launched from either task list or Recent Items menu. However, if the application requires that sub-flows can also be bookmarked and be launched from Recent Items menu. Then this sub-flow design pattern can be utilized for that functionality.

If sub-flows are bookmarked, it can be relaunched from Recent Items menu. In the sub-flows, it's required that users can also navigate back to its parent flow. Sub-flow design pattern also takes that into consideration.

Overview

To record sub-flows into the Recent Items list, applications need to call openSubTask API right before sub-flows are launched[2]. openSubTask takes parameters similar to openMainTask's. One of them is task flow ID. For this, you need to specify parent flow's ID (or main task's ID). In other words, sub-flows need to be executed via parent flow even they are launched from Recent Items menu. See Sample Implementation section for details.
If your sub-flow doesn't need to be bookmarked by Recent Items, you don't need to change anything. Otherwise, you need to modify your parent flow and sub-flow as described in the following task. After the changes, sub-flows can be launched in two ways:
  1. From original flows
  2. From Recent Items menu items using recorded information
Both will start the execution in parent flow. Because sub-flow needs to be launched via parent flow in the 2nd case above, you need to change parent flow in this way:
  1. Add a new router activity at the beginning of the parent flow. Based on a test condition (to be described later), it will route the control to either the original parent flow or the task flow call activity (i.e., the sub-flow).
  2. Add an optional method call activity to initialize sub-flow before it's launched for the 2nd case (i.e., launching from Recent Items menu). Fusion developers can code the method in such a way that it can navigate to the sub-flow after initializing the parent state. This allows applications to render contextual area, navigating back to parent flow from sub-flow and any other customizations.
  3. Bind openSubTask to the command component (i.e., link or button) which causes the flow navigate to the task flow call activity in the original parent flow. openSubTask API registers the parent flow details (to be launched as a sub-flow later) to the Applications Core task flow history stack.
Usually, you don't need to modify your sub-flow for this task. However, you can consolidate the initialization steps from two execution paths in such a way:
  1. Remove initialization parts from both paths in the parent flow. Instead set input parameters (which to be used as test conditions in sub-flows) in both paths only.
  2. Modify sub-flow to take input parameters.
  3. Add a new method call (say initSubFlow) at beginning of the sub-flow to initialize states in parent flow (for example, parent table) so that sub-flow can be launched in the appropriate context.
Note that the design pattern also requires the application capable of navigating back to parent flow from sub-flow. So, the initialization code should take this into consideration (i.e., set up states to allow sub-flow to navigate back) too.
In the following, we'll use an Employee sample implementation to demonstrate the details of this design pattern.

Sample Implementation

In this Fusion Web Application, users select Subflow Design Pattern from the Task list. They then specify some criteria for searching a specific employee or employees. From the list, they can choose the employee that they want to show the details for. This procedure is demonstrated in the following screen shots:
Ename in the search result table is a link which can be used to navigate to the employee detail page of a specific employee. When this link is clicked, a sub-flow (or nested bounded task flow) is called and it displays the Employee Complete Detail page.

If users would like to add this Employee Complete Detail page of a specific employee (say, employee named 'Allen') to their Recent Items list, application developers need to set up something extra to make this happen. If this page (actually what gets recorded is a bounded task flow whose default page is displayed) has been bookmarked, next time users can click it on the Recent Items menu and launch it directly by skipping the search step (i.e., identify the Employee whose details need to be displayed).

Implementation Details

Our parent task flow named ToParentSFFlow is shown below:


decideFlow in the diagram is the router activity that decides whether the control flow should go to either original parent flow path (i.e., "initParent") or sub-flow path (i.e., "toChild"). The condition we used is defined as follows:
<router id="decideFlow">
 <case>
   <expression>#{pageFlowScope.Empno == null}</expression>
   <outcome id="__9">initParent</outcome>
 </case>

 <case>
   <expression>#{pageFlowScope.Empno != null}</expression>
   <outcome id="__10">toChild</outcome>
 </case>

 <default-outcome>initParent</default-outcome>
</router>
In the test, we check whether Empno variable in the parent flow's pageFlowScope is null or not. #{pageFlowScope.Empno} is set via its input parameter Empno when parent flow is called . The input parameters on the parent flow (i.e., ToParentSFFlow) is defined as follows:
<input-parameter-definition>
  <name>Empno</name>
  <value>#{pageFlowScope.Empno}</value>
  <class>java.lang.String</class>
</input-parameter-definition>
 
When parent flow is launched from Task List, parameter Empno is not set (i.e., not defined in the Application menu's itemNode). Therefore, it's null and router will route it to "initParent" path.
When sub-flow is recorded via openSubTask API, we set Empno on the parametersList as follows:
<methodAction id="openSubTask" RequiresUpdateModel="true"
                 Action="invokeMethod" MethodName="openSubTask"
                 IsViewObjectMethod="false" DataControl="FndUIShellController"
                 InstanceName="FndUIShellController.dataProvider"
                 ReturnName="FndUIShellController.methodResults.openSubTask_FndUIShellController_dataProvider_openSubTask_result">
     <NamedData NDName="taskFlowId" NDType="java.lang.String"
         NDValue="/WEB-INF/oracle/apps/xteam/demo/ui/flow/ToParentSFContainerFlow.xml#ToParentSFContainerFlow"/>
     <NamedData NDName="parametersList" NDType="java.lang.String"
                NDValue="Empno=#{row.Empno}"/>
     <NamedData NDName="label" NDType="java.lang.String"
                NDValue="#{row.Ename} complete details"/>
     <NamedData NDName="keyList" NDType="java.lang.String"/>
     <NamedData NDName="taskParametersList" NDType="java.lang.String"/>
     <NamedData NDName="viewId" NDType="java.lang.String"
                NDValue="/DemoWorkArea"/>
     <NamedData NDName="webApp" NDType="java.lang.String"
                NDValue="DemoAppSource"/>
     <NamedData NDName="methodParameters"
         NDType="oracle.apps.fnd.applcore.patterns.uishell.ui.bean.FndMethodParameters"/>
</methodAction>
 
We also set up:
  • taskFlowId to be parent flow's, not subflow's
  • label to be subflow's
When end users click on the link (i.e., Ename), which the openSubTask method is bound to, openSubTask will be called. This link component is defined as follows:
<af:column sortProperty="Ename" sortable="false"
           headerText="#{bindings.ComplexSFEmpVO.hints.Ename.label}"
           id="resId1c2">
  <af:commandLink id="ot3" text="#{row.Ename}"
                  actionListener="#{bindings.openSubTask.execute}"
                  disabled="#{!bindings.openSubTask.enabled}"
                  action="toChild">
    <af:setActionListener from="#{row.Empno}"
                          to="#{pageFlowScope.Empno}"/>
  </af:commandLink>
</af:column>


 
Note that when the link is clicked, actionListener and action specified on the link are executed and in that order. Also note that openSubTask needs to be called only from the original parent flow path (i..e, "initParent"), not sub-flow path(i.e., "toChild).
EmployeeeDetails activity in the above figure is a Task Flow Call activity which invokes our sub-flow (i.e., ToChildSFFlow). Before sub-flow is executed, you need to add some initialization steps. These initialization steps could include, but not limited to:
  • Set up parent states. For our example, we need to set selected employee's row to be current.
  • Set up contextual area state.
  • Set up states to allow sub-flow to navigate back to parent flow.
There are two approaches to set up initialization steps:
  1. In the parent flow
  2. In the sub-flow
For the first approach, you can add logic to initialize both paths before the task flow call activity in the parent flow. For the second approach, you initialize states in the sub-flow by using input parameters of the sub-flow. For example, in our example, sub-flow will take an input parameter named Empno. So, the second approach just postpone the initialization to the sub-flow.
Let's see how input parameters are defined in Task Flow Call activity and sub-flow.
Here is the definition of input parameters in our Task Flow Call activity:

<task-flow-call id="EmployeeDetails">
     <task-flow-reference>
       <document>/WEB-INF/oracle/apps/xteam/demo/ui/flow/ToChildSFFlow.xml</document>
       <id>ToChildSFFlow</id>
     </task-flow-reference>
     <input-parameter>
       <name>Empno</name>
       <value>#{pageFlowScope.Empno}</value>
     </input-parameter>
</task-flow-call>

 
Note that this means that the calling task flow needs to store the value of Empno in #{pageFlowScope.Empno}. For example, from the original parent flow path, it is set to be #{row.Empno} using setActionListener tag. For the sub-flow path, it is set using parent flow's input parameter named Empno. On the sub-flow, we need to specify its input parameters as below:
<task-flow-definition id="ToChildSFFlow">
   <default-activity>TochildSFPF</default-activity>
   <input-parameter-definition>
     <name>Empno</name>
     <value>#{pageFlowScope.Empno}</value>
     <class>java.lang.String</class>
   </input-parameter-definition>
   ...
</task-flow-definition>
Note that the name of the input parameter (i.e., "Empno") needs to be the same as the parameter name defined on the task flow call activity. When parameter is available, ADF will place it in:
#{pageFlowScope.Empno}
to be used within sub-flow. However, this pageFlowScope is different from the one defined in the Task Flow Call activity because they have different owning task flow (i.e., parent task flow vs. sub-flow).
Here is the definition of sub-flow:
In the sample implementation, we chose to implement the initialization step in the sub-flow. Empno is passed as an parameter to sub-flow and used to initialize parent state. When sub-flow is launched, default view activity (i.e., ToChildPF) displays. Before it renders, initPage method on the ChildBean will be executed first. The page definition of the default page is defined as follows:
<pageDefinition xmlns="http://xmlns.oracle.com/adfm/uimodel">
 <parameters/>
 <executables>
   ...
   <invokeAction id="initPageId" Binds="initPage" Refresh="always"/>
 </executables>
 <bindings>
   ...
   <methodAction id="initPage" InstanceName="ChildSFBean.dataProvider"
                 DataControl="ChildSFBean" RequiresUpdateModel="true"
                 Action="invokeMethod" MethodName="initPage"
                 IsViewObjectMethod="false"
                 ReturnName="ChildSFBean.methodResults.initPage_ChildSFBean_dataProvider_initPage_result"/>
    ...
 </bindings>
</pageDefinition>

As shown above, initPage is specified in the executables tag and will be invoked when the page is refreshed. initPage method itself is defined as follows:

public void initPage()
{
   FacesContext facesContext = FacesContext.getCurrentInstance();
   ExpressionFactory exp = facesContext.getApplication().getExpressionFactory();
   DCBindingContainer bindingContainer =
     (DCBindingContainer)exp.createValueExpression(
         facesContext.getELContext(),"#{bindings}",DCBindingContainer.class).getValue(facesContext.getELContext());
   ApplicationModule am = bindingContainer.getDataControl().getApplicationModule();

   ViewObject vo = am.findViewObject("ComplexSFEmpVO");
   vo.executeQuery();

   Map map = AdfFacesContext.getCurrentInstance().getPageFlowScope();
   if(map !=null){
        Object empObj = map.get("Empno");
        if(empObj instanceof Integer){
            Integer empno =(Integer)map.get("Empno");// new Integer(empnoStr);
            Object[] obj = {empno};
            Key key = new Key(obj);
            Row row = vo.getRow(key);
            vo.setCurrentRow(row);
        }
        else
        {
            String empnoStr = (String)map.get("Empno");
            Integer empno = new Integer(empnoStr);
            Object[] obj = {empno};
            Key key = new Key(obj);
            Row row = vo.getRow(key);
            vo.setCurrentRow(row);
        }
    }
}
In initPage, it takes input parameter Empno (i.e., from #{pageFlowScope.Empno}) as a key to select a row and set it to be the current row in the master table (i.e., Employee table).
References
  1. Oracle® Fusion Applications Developer's Guide 11g Release 1 (11.1.1.5)
  2. openSubTask and closeSubTask APIs
  3. Oracle ADF Task Flow in a Nutshell