This article is a quick note on how to validate an input field added to an amx:listView and how to reset the list item value back to the original value. The use case in the sample code snippet is a field service application that technicians use to protocol service incidents, the status of a repair and the time spent on it. The validation should be such that the time the technician worked on an incident doesn't go beyond the 8 hours of the working shift.

About the Data Model

I like the Mobile Application Framework  data control as an abstraction layer, but I also like to be in control. Because of this, independent of the service or data source (SQLite) to access, I always work through a POJO model. For this sample, the simple POJO model I created includes to classes.

The entity

 public class ServiceAssignment {
    
    String orderId = null;
    String customerName = "";
    String incident = "";
    String repairStatus="";
    Integer repairHours = 0;
    private PropertyChangeSupport propertyChangeSupport = 
                              new PropertyChangeSupport(this);
    public ServiceAssignment() {
        super();
    }
    public void setOrderId(String orderId) {
        String oldOrderId = this.orderId;
        this.orderId = orderId;
        propertyChangeSupport.firePropertyChange("orderId", oldOrderId, orderId);
    }
    public String getOrderId() {
        return orderId;
    }
    public void setCustomerName(String customerName) {
        String oldCustomerName = this.customerName;
        this.customerName = customerName;
        propertyChangeSupport.firePropertyChange("customerName", 
                                  oldCustomerName, customerName);
    }
    public String getCustomerName() {
        return customerName;
    }
    public void setIncident(String incident) {
        String oldIncident = this.incident;
        this.incident = incident;
        propertyChangeSupport.firePropertyChange("incident", 
                                          oldIncident, incident);
    }
    public String getIncident() {
        return incident;
    }
    public void setRepairStatus(String repairStatus) {
        String oldRepairStatus = this.repairStatus;
        this.repairStatus = repairStatus;
        propertyChangeSupport.firePropertyChange("repairStatus", 
                                 oldRepairStatus, repairStatus);
    }
    public String getRepairStatus() {
        return repairStatus;
    }
    public void setRepairHours(Integer repairHours) {
        Integer oldRepairHours = this.repairHours;
        this.repairHours = repairHours;
        propertyChangeSupport.firePropertyChange("repairHours",
                                 oldRepairHours, repairHours);
    }
    public Integer getRepairHours() {
        return repairHours;
    }
    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }
    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }
}

The facade (becoming the data control)

public class TechnicianAssignments {

    protected transient ProviderChangeSupport   providerChangeSupport 
                                     = new ProviderChangeSupport(this);  
    private ArrayList<ServiceAssignment> assignments = 
                                        new ArrayList<ServiceAssignment>();
    private PropertyChangeSupport propertyChangeSupport = 
                                          new PropertyChangeSupport(this);
    public TechnicianAssignments() {
        super();

        ServiceAssignment sa1 = new ServiceAssignment();
        sa1.setOrderId("1");
        sa1.setIncident("washing machine - short circuit");
        sa1.setRepairStatus("fixed");
        sa1.setRepairHours(new Integer(1));
        sa1.setCustomerName("John Muller");
        ServiceAssignment sa2 = new ServiceAssignment();
        sa2.setOrderId("2");
        sa2.setIncident("TV set broken");
        sa2.setRepairStatus("fixed");
        sa2.setRepairHours(new Integer(3));
        sa2.setCustomerName("Bill Spender");
        ServiceAssignment sa3 = new ServiceAssignment();
        sa3.setOrderId("3");
        sa3.setIncident("Cofee machine doesn't clean");
        sa3.setRepairStatus("pending");
        sa3.setRepairHours(new Integer(0));
        sa3.setCustomerName("Gaby Crown");
        ServiceAssignment sa4 = new ServiceAssignment();
        sa4.setOrderId("4");
        sa4.setIncident("dish washer - leaks water");
        sa4.setRepairStatus("fixed");
        sa4.setRepairHours(new Integer(5));
        sa4.setCustomerName("John King");
        assignments.add(sa1);
        assignments.add(sa2);
        assignments.add(sa3);
        assignments.add(sa4);
    }
    public void setAssignments(ArrayList<ServiceAssignment> assignments) {
        ArrayList<ServiceAssignment> oldAssignments = this.assignments;
        this.assignments = assignments;
        propertyChangeSupport.firePropertyChange("assignments", 
                                       oldAssignments, assignments);
    }
    public void refreshAssignmentsList(){
        //refresh the list for the UI to refresh
        propertyChangeSupport.firePropertyChange("assignments", 
                                           null, assignments);
    }
    public ArrayList<ServiceAssignment> getAssignments() {
        return assignments;
    }
    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }
    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }
    public void addProviderChangeListener(ProviderChangeListener l) {  
         providerChangeSupport.addProviderChangeListener(l);  
      }  
      public void removeProviderChangeListener(ProviderChangeListener l) {  
         providerChangeSupport.removeProviderChangeListener(l);  
      }  
}

Please have a special interest in the following method added to the data control facade:

 

public void refreshAssignmentsList(){
        //refresh the list for the UI to refresh
        propertyChangeSupport.firePropertyChange("assignments", null, assignments);
    }

This method invokes the provider change event for the "assignments" list that then propagates the event notification to the list for the  list view to refresh. This is important to ensure that the value change issued by the validation is displayed on the screen.

The View

The amx view renders a list based on the "assignments" collection that is exposed on the data control.

<amx:listView var="row" value="#{bindings.assignments.collectionModel}"
                  fetchSize="#{bindings.assignments.rangeSize}"
                  selectedRowKeys="#{bindings.assignments.collectionModel.selectedRow}"
                  selectionListener="#{bindings.assignments.collectionModel.makeCurrent}" 
                  showMoreStrategy="autoScroll"
                  bufferStrategy="viewport" id="lv1">
      <amx:listItem id="li1"> ... </amx:listItem>
</amx:listView

 The list view is configured such that selecting a list item will automatically (or automagically) set the selected row as current in the MAF binding layer. The input component that exposes the working hours gets a value change listener that points to a managed bean to evaluate the user entry and – if needed – to correct it.

The managed bean code

The managed bean can live in viewScope as there is no state it needs to keep across page navigation. The code simply checks whether the user provided value is above 8 hours or below 0 (though I never experienced a field service to not at least charge for one hour. So probably 0 is for warranty cases)

public class RepairAssignmentsBacking {
    public RepairAssignmentsBacking() {
    }
    public void onRepairHoursChange(ValueChangeEvent valueChangeEvent) {
          Integer newVal = new Integer((String) valueChangeEvent.getNewValue());
          Integer oldVal = new Integer((String) valueChangeEvent.getOldValue());

          //validation condition
          if(newVal>8 || newVal<0){
             BasicIterator iterator = (BasicIterator) AdfmfJavaUtilities.getELValue
                                      ("#{bindings.assignmentsIterator.iterator}");          
             ServiceAssignment sa = (ServiceAssignment) iterator.getDataProvider();
             sa.setRepairHours(oldVal);
             DataControl dc = AdfmfJavaUtilities.getDataControlById(
                                 iterator.getDataControl().getName());
             TechnicianAssignments dcImpl = (TechnicianAssignments)dc.getDataProvider();
             //refresh UI
              dcImpl.refreshAssignmentsList();
              throw new AdfException("The working time cannot be greater than 8 hours", AdfException.ERROR);
          }
    }
}

As you can see, the trick really is to know about the iterator that servers the values for the listView component. If te underlying data control is POJO based, then accessing the base iterator (shown in the code above) and calling getDataProvider() gives you access to the entity bean that represents the row. This is where you perform the update.

But this is not the whole 9 years we need to go to get the use case working. After updating the entity you need to refresh the collection the entity is located in. For this, a method has been exposed on the POJO DC (explained earlier) to trigger the refresh of the collection by invoking the provider change event for the "assignments" property.

DataControl dc = AdfmfJavaUtilities.getDataControlById(
                   iterator.getDataControl().getName());
TechnicianAssignments dcImpl = (TechnicianAssignments)dc.getDataProvider();
//refresh UI
dcImpl.refreshAssignmentsList();

Btw.: If you don't like the end user notification via an ADFException, which I agree is not a good user experience, you can

i) define a managed bean and an error message component / area in the amx. The managed bean is updated from the validator and thanks to property change listeners would update the error message instantly.

Or, ii) you use a custom popup as explained here https://blogs.oracle.com/mobile/entry/how_to_open_and_close

—-

QUIZ YOURSELF: There is a design flaw in the code above! Can you spot it? No? Then read on.

—-

A Problem you may run into with this …

The managed bean code so far assumes that the input data is validated while the row currency doesn't change. This however may happen if validation is in a value change listener and thus – for the use case I describe – can be considered a design flow in my sample.

 

So lets fine tune the sample code to ensure it works even if the row currency canges to avoid updating the wrong row. To avoid the problem, you add a set property listener to the list item in the view to memorize the selected row in the managed bean. This way, the validation code can explicitly set the current row in the model to the row that the changes should be applied to.

Step 1 Add a PropertlyListener to the list item

    <amx:listItem ...
        <amx:setPropertyListener id="spl1" 
                                 from="#{row.rowKey}" 
                                 to="#{viewScope.RepairAssignmentsBacking.rowKey}"
                                 type="action"/>
      </amx:listItem

The row key of a list item is set to a property defined in the managed bean that also performs the validation. Because the bean is in view scope, the property value becomes accessible to the validation code

Step 2 Set the current row

if(newVal>8 || newVal<0){
      BasicIterator iterator = (BasicIterator) AdfmfJavaUtilities.getELValue(
                                   "#{bindings.assignmentsIterator.iterator}");    
      //in case of row currency change, this is the key of the newly selected row
      Object currentRowKey = iterator.getCurrentRowKey();
      //set the current row back to the row that the values should be reset for
      iterator.setCurrentRowWithKey((String) this.rowKey);
      ServiceAssignment sa = (ServiceAssignment) iterator.getDataProvider();
      sa.setRepairHours(oldVal);
      //now set the current row back to the row the user navigated to (if at all)
      iterator.setCurrentRowWithKey((String) currentRowKey);            
      DataControl dc = AdfmfJavaUtilities.getDataControlById(
                                      iterator.getDataControl().getName());
      TechnicianAssignments dcImpl = (TechnicianAssignments)dc.getDataProvider();
      //refresh UI
      dcImpl.refreshAssignmentsList();
     throw new AdfException("The working time cannot be greater than 8 hours"
                            , AdfException.ERROR);
 }

 

The Complete Managed Bean Code (with the change)

The corrected managed bean code shown below highlights the changes in bold. 

 public class RepairAssignmentsBacking {
    
    private Object rowKey = null;
    private PropertyChangeSupport propertyChangeSupport = 
                                          new PropertyChangeSupport(this);
    public RepairAssignmentsBacking() {
    }
    public void onRepairHoursChange(ValueChangeEvent valueChangeEvent) {
        
          Integer newVal = new Integer((String) valueChangeEvent.getNewValue());
          Integer oldVal = new Integer((String) valueChangeEvent.getOldValue());
        
          
          //validation condition
                    if(newVal>8 || newVal<0){
             BasicIterator iterator = 
            (BasicIterator) AdfmfJavaUtilities.getELValue(
                             "#{bindings.assignmentsIterator.iterator}");    
             //in case of row currency change, this is the key of the newly selected row Object currentRowKey = iterator.getCurrentRowKey();
             //set the current row back to the row that the values should be reset for iterator.setCurrentRowWithKey((String) this.rowKey);
             ServiceAssignment sa = (ServiceAssignment) iterator.getDataProvider();
             sa.setRepairHours(oldVal);
             //now set the current row back to the row the user navigated to (if at all)iterator.setCurrentRowWithKey((String) currentRowKey);            
             DataControl dc = AdfmfJavaUtilities.getDataControlById(
                                                    iterator.getDataControl().getName());
             TechnicianAssignments dcImpl = (TechnicianAssignments)dc.getDataProvider();
             //refresh UI
              dcImpl.refreshAssignmentsList();
              throw new AdfException("The working time cannot be greater than 8 hours", 
                                     AdfException.ERROR);
          }
    }
    public void setRowKey(Object rowKey) {
        Object oldRowKey = this.rowKey;
        this.rowKey = rowKey;
        propertyChangeSupport.firePropertyChange("rowKey", oldRowKey, rowKey);
    }
    public Object getRowKey() {
        return rowKey;
    }
    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }
    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }
}

 

Author