Train Stop Status Handling

A question on the Oracle Developers Community was about how to handle a train stops visited status.

Use Case

The use case behind this was that a train can be used as a workflow visualization. A normal user starts the train, but at one point a manager has to approve something. This approval is one or more stops on the same train. If the manager picks up the workflow he should automatically start with the approval stop. There is no need for him to see the data accumulated in the stops before.

The use case has multiple challenges:

  1. Securing train stops for different user roles
  2. Allow starting the train from any stop
  3. Handling the state of the train stops

The first two challenges are handler by All Aboard, 97. How-to defer train-stop navigation for custom form validation or other developer interaction, and 82. How to programmatically navigate ADF trains.

The missing part is how to handle the train stops ‘visited’ state (see image above). If you start the train directly with ‘Stop 3’ you get this state

UI

To implement this use case, we use a simple UI. It contains an input field, a button and the train which is added to the page as a region.

In the input field names label 1 you can enter the stop where the train should start. If no number is given, the train starts with the first stop. We use this input field to mimic the different starting stop for different users. This is the page when we start the application:

This is the page when we start the final application:

You can navigate between the train stops by using the ‘Back’ and ‘Next’ button, or by clicking the next stop in the train bar. As the stops are set to sequential, you can’t directly click on the 4th stop. You have to go through the stops 1 to 3 first.

Enter a number between 1 and 5 into the input field and tab out of the field will set the parameter for the train task flow and restart the task flow. The navigation is done via a router in the task flow. In the image below the stop number 3 is set as the starting stop for the train

And as you see the stops 1 and 2 are looking like they have visited before.

Implementation

To show how to implement this we start with a simple bounded task flow which builds the train

The start builds a router which we use to navigate to the stop where we want to start the train. The starting stop is passed as parameter to the task flow

In the router, which is marked as default activity, the parameter is used to execute the navigation

The Magic

If you look at the train stop properties in the properties inspector you’ll notice, that there is no property for the visited state

This option is not available in the UI. Oracle has missed or deliberately missed to make this property accessible via the properties. If you dig into the implementation of the train task flow (see the articles provided at the begin of the blog), you’ll see how to access the train and its stops by code:

ViewPortContext currentViewPortCtx = controllerContext.getCurrentViewPort();
TaskFlowContext taskFlowCtx = currentViewPortCtx.getTaskFlowContext();
TaskFlowTrainModel taskFlowTrainModel = taskFlowCtx.getTaskFlowTrainModel();
// get the stop from the map
TaskFlowTrainStopModel currentStop = taskFlowTrainModel.getCurrentStop();

The TaskFlowTrainStopModel doesn’t provide any access to the visited state. If you look at the class definition you’ll notice, that it’s only an interface

which doesn’t provide access to the visited property. Setting a breakpoint in the debugger we can inspect an instance of this interface

and we get the class implementing the interface as:

 oracle.adfinternal.controller.train.TrainStopModel

This class has the visited property we are looking for.

Solution

Now we can implement a method which we call before a train stop gets rendered and which sets the visited property of all previous stops to true.

CAUTION

THIS IN AN INTERNAL CLASS WHICH YOU SHOULD NOT USE!

However, it’s the class we need to get to the property. You have to understand, that the usage of the class has its risks, but that it’s not forbidden. The risk is that Oracle can change or delete the class without notifying you beforehand. So, in later versions, your code might break.

The method checks the task flow parameter if it’s null to set to a number less or equal to 0. In this case, the method returns an empty string. We do this check to avoid that the method does it’s work every time we navigate the train. It should be done only once when the train starts.

If the check finds a positive number, it sets the task flow parameter to zero (line 37).

It then gets the task flow information from the Context (lines 39-43). In line 50 we acquire the current stop before we loop over all previous stops and set their visited property to true (lines 53-59).

The missing part is how to call this method when a train stop is rendered. For this, we use a technique called Lazy Initalizing Beans. The trick is to use a hidden af:outputText and set e.g. the value property of the component to a bean property.

When the page or fragment is rendered, the method getInitStatus() in the bean is called. This is exactly the method shown above. We add this hidden af:outputText to each train stop before the af:train component.

Sample

You can download the sample from GitHub BlogTrainStopStatus. The sample is build using JDev 12.2.1.3 and doesn’t need a DB connection. You can use the same technique in other JDeveloper versions.

JDeveloper 11.1.1.5.0 : Use Router to Create New Row or Edit Existing

Based on a question on the OTN ADF forum I wrote a small sample to show how to use a router in a bounded task flow to either edit an existing row, or create a new row and present it to enter data.

Use Case
Based on a query which returns either zero or one row an edit form should should be presented to the user. This form show the data from the row found, or an empty form to allow creation of a new row.

Implementation
The sample workspace, which you can download from the ADF Samples webside using the link at the end of this post, uses a very simple model. It contains only one view object, based of the LOCATIONS table of the HR schema.
The view object has one custom method implemented which search for a location record by an id. This method is used as query which returns either one row (if the location is found) or no row (if the location is not found).
The UI is build with one page which has a splitter component. On the left facet we find an simple form which we use to input the location id to search for and a button to execute the query. To allow to save or cancel the actions we add a rollback and a commit button. On the second facet we see a region which shoes the panelForm to either edit the existing record or to enter data into a new created record.
The image below shows the page layout.

General page layout

General page layout

Running

The next image shows the running application with a location id found

Running application with Location Id found

Running application with Location Id found

and then with a location id not found

Location ID not found

Location ID not found

You clearly see the empty from to enter new row data.

Now, to implement this the first splitter facet has a button to executes the query (find location by id) with the id given in the inputText. The region on the second splitter facet refreshes itself as we set the ‘Refresh’ property to ‘ifNeeded’. To make this work we need to change an input parameter to the region. This is done by binding the input parameter of the region to the inputText component for the location id.

Setup of input parameter of the region

Setup of input parameter of the region

So whenever the input parameter changes the region gets restarted. this effect we use in the bounded task flow to check if we have to create a new row and thus show a empty form, or to just edit an existing row. For this the bounded task flow uses a router component as start activity. This router checks if the iterator of the locations view object contains zero rows or more.

Bounded task flow

Bounded task flow

The router uses the estimatedRowCount of the underlying view object in an expression to check the number of rows:

EL to check if iterator has zero rows

EL to check if iterator has zero rows

The router navigates to the ‘insert’ case if the EL evaluated to true, to the ‘edit’ case otherwise. That concludes the use case.

You can down load the sample work space from ADF Samples Repository. The sample was build using JDeveloper 11.1.1.5.0 but should work using 11.1.1.6.0 and 11.1.2.x too. It uses the HR schema and you need to correct the db credentials to run the sample.

JDEV 11.1.2.1.0: Using router to conditionally set navigation target

Interesting question came up on OTN ADF froum.
You have one page (page2) which is called from two other pages (page1 and page3). The question is how to set up the navigation in page2 so that you have only on button (back) which gets you back to the page the user navigated from to page2?

There is more then one solution to this problem. In this blog entry I show the declarative solution, so no java code is used. Here is adfc-config.xml which shows the navigation:

Router Back Navigation

Router Back Navigation

This is a simple navigation where the user can navigate from Page1 or Page3 to Page2. Page2 uses only one navigation case ‘back’ and let the router decide where to go. To make this happen, the button which navigates from Page1 to Page2 needs to store a hint ‘1’ in pageFlowScope which the router can check to decide where to go to. For this I add a af:setPropertyListener to store the hint in pageFlowScope. You don’t need a bean to store the value as the storage for the value is set up automatically.

                        <af:commandButton text="Page 2" id="cb1" action="page12">
                            <af:setPropertyListener from="#{'1'}" to="#{pageFlowScope.backTarget}"
                                                    type="action"/>
                        </af:commandButton>

The same technique is used in Page3 to store the hint ‘3’ in pageFlowScope

                        <af:commandButton text="Page 2" id="cb1" action="page32">
                            <af:setPropertyListener to="#{pageFlowScope.backTarget}" type="action"
                                                    from="#{'3'}"/>
                        </af:commandButton>

Finally the magic is done in the router. Here the value which is stored in pageFlowScope variable ‘backTarget’ is checked and hte correct navigation target is used for the back navigation. Below is the source of the router, the design view is shown in the first picture.

  <router id="backRouter">
    <case id="__12">
      <expression>#{pageFlowScope.backTarget eq '1'}</expression>
      <outcome>page21</outcome>
    </case>
    <case id="__13">
      <expression>#{pageFlowScope.backTarget eq '3'}</expression>
      <outcome>page23</outcome>
    </case>
    <default-outcome>page21</default-outcome>
  </router> 

When you run the sample you start from Page1 and navigate to Page2 you see the output text which shows the content of the pageFlowScope variable, ‘1’ in this case.

Navigation from Page1 to Page2

Navigation from Page1 to Page2

If the user navigates from Page3 to Page2 the output looks like

Navigation Page3 to Page2

Navigation Page3 to Page2


the output text shows ’3′ in this case.

You can download the sample application which is build using JDeveloper 11.1.2.1.0 from here BlogRouterBackNavigation.zip.
Please rename the file to ‘.zip’ after downloading it!
The sample don’t use any db connection and should be runnable on older JDeveloper versions too.