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.

Advertisements

JDeveloper: Using Task Flow Parameters to Show Different UI in a Region

Lately a couple of questions on the JDeveloper & ADF space regarding using task flow parameters came up.

Use Case

One specific use case was how to show different UI in the same region if a row is just created or if the user wants to edit an already existing row.

Full description is that the user sees a table with e.g. regions of the HR DB schema. Now there are two buttons, one ‘Create new…’ and one ‘Edit current…’. When clicking the ‘Edit current…’ button the currently selected row of the table should be loaded into a form. There the user can edit everything but the primary key (PK). If the user click the ‘Create new…’ button the same form should be visible, but the PK should be editable too.

Running Application

To make it more visible let’s start with the finished application:

selection_955

Running Application

The final UI looks like in the image above. The UI is composed of four areas as in the image below:

selection_955_comment

The ‘Header’ and ‘Search Panel’ area are only for convenience. In the ‘Panel Collection Bar’ holds a toolbar with two buttons ‘Create new…’ and ‘Edit current…’. The table below shows the result of a search of the Region table from the HR DB.

Selecting a row in the table we can edit the selected record by clicking on the ‘Edit current…’ button

selection_956

This will open a new screen showing the selected row. Above the ‘RegionId’ we see a text indicating that we are in ‘edit’ mode and we can’t edit the ‘RegionId’ attribute as it’s the PK of the row and should not be editable.

Here we can edit the RegionName attribute and store the change by clicking the ‘Commit’ button:

Likewise, if we click the ‘Create new…’ button we go to the same form, but this time the text above the ‘RegionId’ attribute tells us that we are in ‘create’ mode and we can edit the RegionId.

Committing the changes we get a new row in the Regions HR DB table.

Implementation

Ok, let’s talk about how to implement this. For the model layer we run the ‘Business Components from Table…’ wizard on the model project and select the regions table from the HR DB. For this demo this is all we need to do.

The UI consist of two pages, index.jsf and Region.jsf. The index.jsf page is the start page and shows the UI as in the first image. Everything is easily done by drag and drop the right components in the right order onto the page. I spare the details for this as you can look at the sample which you can download using the link at the end of the post.

The only thing I like to go into detail is the toolbar with the two buttons ‘Create new…’ and ‘Edit current…’. These buttons do two things:

  1. Set a mode property to pageFlowScope
  2. Navigate to the second page Region.jsf

The Toolbar definition looks like

 <af:toolbar id="t1">
   <af:button text="Create new..." id="b1" action="show">
     <af:setPropertyListener from="#{'create'}" to="#{pageFlowScope.mode}" type="action"/>
   </af:button>
   <af:button text="Edit current..." id="b2" action="show">
     <af:setPropertyListener from="#{'edit'}" to="#{pageFlowScope.mode}" type="action"/>
   </af:button>
 </af:toolbar>

The create button has a af:setPropertyListener added which sets a pageFlowScope attribute ‘mode’ to ‘create’ and navigates to the Region.jsf page by executing the ‘show’ navigation from the unbounded task flow adfc-config.xml

adfc-config.xml

adfc-config.xml

The edit button uses an af:setPropertyListener which sets a pageFlowScope attribute ‘mode’ to ‘edit’ and then executes the navigation ‘show’ to go to the Region.jsf page. The logic to insert a new row or to edit an existing row is done in the bounded task flow ‘region-edit-create-btf.xml’ which we talk about later.

The Region.jsf page consists of a Header and a Region holding an af:form of the selected row of the Region:

selection_957_comment

Region.jsf

The region itself is a bounded task flow with the following properties

selection_964

Here we see one parameter with the name ‘mode’ which stores its value in a pageFlowScope attribute named ‘mode’. One other thing we need to make sure of is that the region shares the data control with its parent (in this case the adfc-config unbounded task flow) and always begins a new transaction. This make the bounded task flow a unit of work, it encapsulates the work in the task flow. The interface of the bounded task flow describes what the unit of work does:

Interface of ‘region-edit-create-btf.xml’ task flow:

If mode is set to ‘edit’, the current selected row of the Region table is shown in a form and can be edited. 

If the mode is set to ‘create’, a new row is created and inserted into the Region table and can then be edited.

The user can commit or cancel the operation. After each of this operations the task flow executes a parent action ‘back’.

selection_965

We see that the default action of the task flow is a router which uses the parameter set to the task flow to execute the create of the edit navigation:

selection_966

after that the now current record is shown on the fragment (see the area marked ‘Region’ in image Region.jsf). Below we see the panelFormLayout used for the region:

 <af:panelFormLayout id="pfl1">
   <af:outputText value="we are in #{pageFlowScope.mode eq 'create'? 'create' : 'edit'} mode" id="ot1"/>
   <af:inputText value="#{bindings.RegionId.inputValue}" label="#{bindings.RegionId.hints.label}"
     required="#{bindings.RegionId.hints.mandatory}" columns="#{bindings.RegionId.hints.displayWidth}"
     maximumLength="#{bindings.RegionId.hints.precision}" shortDesc="#{bindings.RegionId.hints.tooltip}" id="it1"
     disabled="#{pageFlowScope.mode ne 'create'}">
     <f:validator binding="#{bindings.RegionId.validator}"/>
     <af:convertNumber groupingUsed="false" pattern="#{bindings.RegionId.format}"/>
   </af:inputText>
   <af:inputText value="#{bindings.RegionName.inputValue}" label="#{bindings.RegionName.hints.label}"
     required="#{bindings.RegionName.hints.mandatory}" columns="#{bindings.RegionName.hints.displayWidth}"
     maximumLength="#{bindings.RegionName.hints.precision}" shortDesc="#{bindings.RegionName.hints.tooltip}" id="it2">
     <f:validator binding="#{bindings.RegionName.validator}"/>
   </af:inputText>
   <f:facet name="footer">
     <af:panelGroupLayout id="pgl2">
       <af:button text="Commit" id="b2" action="commit"/>
       <af:button text="Rollback" id="b1" immediate="true" action="rollback">
         <af:resetActionListener/>
       </af:button>
     </af:panelGroupLayout>
   </f:facet>
 </af:panelFormLayout>

Let’s look at the actions which are done in the region. If the user commits the changes the commit action from the data control is called which saves the changes to the db. If the ‘cancel’ button is clicked, the rollback method from the data control is called which reverts any changes done in the task flow. After the commit or rollback a parentAction (paraneAction1) is called which executes the ‘back’ navigation in the adfc-config.xml which navigates back to the index.jsf page.

Please note that we could have added the calls to commit and rollback to the buttons in the region.jsff. I decided to put them into the task flow instead to show the whole task flow and how it works in one place.

Implement different UI according to the task flow parameter

So, how do we use the parameter passed to the bounded task flow to switch the UI?

This is done by using an expression language (EL) which points to the ‘mode’ attribute stored in the pageFlowScope. Sample: the text above the RegionId is created with an af:outputText like

<af:outputText value="we are in #{pageFlowScope.mode eq 'create'? 'create' : 'edit'} mode" id="ot1"/>

The EL ‘#{pageFlowScope.mode eq ‘create’? ‘create’ : ‘edit’} ‘ is used to differentiate between the modes. Likewise the disable property of the RegionId attribute uses the EL

...disabled="#{pageFlowScope.mode ne 'create'}"...

which is true when the passed parameter is not ‘create’. In this case the disabled property is set to false, meaning that the field can’t be edited.

That’s it. There is no line of java code necessary to implement this use case.

Download

You can download the sample which was build using JDeveloper 12.2.1.2 and uses the HR DB schema from GitHub BlogTaskFlowParameter.