Code Walk Thru

Home.About Strandz.Example.Code Walk Thru
Home Forums Glossary Make Contact


The Model

We are going to do a bottom-up tour of the Wombat Rescue Rostering Application user interaction code. Before we start the model needs to be vaguely understood. These two diagrams show all the nodes in RosterWorkersStrand and the cells inside WorkerNode:

This reference table completes our look at the model. It is a list of all the attributes, taken from the source code file applic.wombatrescue.RosterWorkersDT. ('DT' stands for design-time). In the diagrams we did not show any of the detail inside rosterSlotReferenceDetailNode - hopefully the table will flesh things out where necessary. Now would be a good time to bring up the application.

Calculate the RosterSlot sentence

applic.wombatrescue.RosterWorkersTriggers is the only triggers file for Wombat Rescue. In this file the model is accessed via a variable called dt:

  private RosterWorkersDT dt;

Thus dt is used to access SdzBeans. In this excerpt we call the method getItemValue() on a cell:

  /*
   * No roster slot actually exists, yet one is manufactured from
   * all the items - and that includes many DOs. So you end up
   * with a 'deep' roster slot with all its object references.
   */
  rosterSlot =
      (RosterSlot)dt.rosterslotReferenceDetailCell.getItemValue();

As the comment explains, all the values on the screen for a particular visible record are used to manufacture a new DO graph.

When the user has inserted a new visible record then the strand is in a NEW state (coded as stateId.isNew()). In this state no new DOs have yet been created: internally Strandz only creates the DO graph when the user motions away from the current visible record, causing a state transition away from NEW.

In the following code we manufacture a RosterSlot if one is currently not available. Once we have the rosterSlot we call the method toSentence() on it, and use the result to set the text of the calculated label:

private void calculateRSSentence(StateEnum stateId)
{
    if(dt.ui1 != null) //possible that a dt would not have any panels
    {
        if(dt.ui1.getLSentence() != null)
        {
            RosterSlot rosterSlot = null;
            if(stateId.isNew())
            {
                /*
                * No roster slot actually exists, yet one is manufactured from
                * all the items - and that includes many DOs. So you end up with
                * a 'deep' roster slot with all its object references.
                */
                rosterSlot = (RosterSlot) dt.rosterslotReferenceDetailCell.getItemValue();
            }
            else
            {
                VisibleExtent ve = dt.rosterslotReferenceDetailCell.getDataRecords();
                int row = dt.rosterslotReferenceDetailNode.getRow();
                if(row != -1) //when stateId == StateEnum.FROZEN/UNKNOWN row will be -1
                {
                    rosterSlot = (RosterSlot) ve.get(row);
                }
            }
            if(rosterSlot != null)
            {
                String sentence = rosterSlot.toSentence();
                Err.pr(SdzNote.tightenRecordValidation, "SENTENCE (from creational method) : " + sentence);
                dt.ui1.getLSentence().setText(NameUtils.toHTML(sentence));
            }
            else
            {
                dt.ui1.getLSentence().setText("");
            }
        }
        else
        {
            Err.error("If have a panel then it s/have a sentence label. dt config problem");
        }
    }
}

From above we can see that when the strand's state is not NEW it is possible to use the model to get the current rosterSlot.

Let us now move on and find out the callers of calculateRSSentence(). There are four altogether. The first invokes the method as the user goes to the rosterslotReferenceDetailNode. Note that if the rosterslotReferenceDetailNode were normally visible to the user then performing the sentence calculation at this point would be too late. In the case where a detail node is always visible you would have to populate the calculated labels as the user navigates on the master node.

sdzBag.getStrand().addPostControlActionPerformedTrigger(
    new PostOperationTrigger() );
...
public class PostOperationTrigger
    implements PostOperationPerformedTrigger
{
    public void postOperationPerformed(OutNodeControllerEvent evt)
    {
        if(evt.getNode() == dt.rosterslotReferenceDetailNode
            && evt.getID() == OperationEnum.GOTO_NODE
            && evt.getNode().getState() != StateEnum.FROZEN)
        {
            workerOps.toggleMonthlyRestart(((Boolean) dt.monthlyRestartAttribute.getItemValue()).booleanValue());
            outer.calculateRSSentence(evt.getNode().getState());
        }
    }
}

The second calculateRSSentence() caller invokes the method as the user navigates on rosterslotReferenceDetailNode:

dt.rosterslotReferenceDetailNode.addNavigationTrigger(
    new RosterSlotNavigationT() );
...
class RosterSlotNavigationT implements NavigationTrigger
{
    public void navigated(OperationEvent evt)
    {
        if(dt.strand.getCurrentNode().getState() != StateEnum.FROZEN)
        {
            outer.workerOps.toggleMonthlyRestart(((Boolean) dt.monthlyRestartAttribute.getItemValue()).booleanValue());
        }
        outer.calculateRSSentence(dt.strand.getCurrentNode().getState());
    }
}

Our third calculateRSSentence() caller invokes the method as a paste operation is being performed:

public class PasteAction extends AbstractAction
{
    public void actionPerformed(ActionEvent ae)
    {
        execute();
    }

    public void execute()
    {
        sdzBag.pasteItemValues();
        workerOps.toggleMonthlyRestart(((Boolean) dt.monthlyRestartAttribute.getItemValue()).booleanValue());
        outer.calculateRSSentence(dt.strand.getCurrentNode().getState());
        setEnabled(false);
    }
}

The ToFrozenTrigger ensures that as the strand transitions to being FROZEN that our last call to calculateRSSentence() occurs. This call is a rather long-winded way of ensuring that the RosterSlotPanel's sentence label is blanked out when no rosterSlots exist for a Worker:

sdzBag.getStrand().addStateChangeTrigger( new ToFrozenTrigger());
...
private class ToFrozenTrigger implements StateChangeTrigger
{
    public void stateChangePerformed(StateChangeEvent e)
    {
        StateEnum current = e.getCurrentState();
        if(current == StateEnum.FROZEN)
        {
            outer.calculateRSSentence(current);
        }
    }
}

Thus calculateRSSentence() is invoked when the user:

  1. Goes to rosterslotReferenceDetailNode.
  2. Navigates between rosterslotReferenceDetailNode visible records.
  3. Pastes a copied rosterSlot to the current one.
  4. Deletes the last rosterSlot.
This gives us the effect having a rosterSlot's sentence up to date at all times except while editing the rosterSlot itself.

Copying and pasting RosterSlots

As we have seen a PasteAction let us explore where it was put together:

private void copyPasteButton(List abilities)
{
    copyAction = new CopyAction();
    copyAction.putValue(Action.NAME, "Copy");
    copyAction.putValue(Action.SHORT_DESCRIPTION,
        "Copy this screen out to the buffer");
    abilities.add(copyAction);
    //
    pasteAction = new PasteAction();
    pasteAction.setEnabled(false);
    pasteAction.putValue(Action.NAME, "Paste");
    pasteAction.putValue(Action.SHORT_DESCRIPTION,
        "Paste to this screen from the buffer");
    abilities.add(pasteAction);
    //
    Err.pr(WombatNote.generic, "copyPasteButton been added");
}

The CopyAction is similar to the PasteAction:

public class CopyAction extends AbstractAction
{
    public void actionPerformed(ActionEvent ae)
    {
        execute();
    }

    public void execute()
    {
        sdzBag.copyItemValues(dt.rosterslotReferenceDetailNode);
        pasteAction.setEnabled(true); // this property is listened to by
                                      // the physical NodeController
    }
}

Thus we can see that the actual mechanics of Copy and Paste are done with simple method calls on the SdzBag. The node parameter in the call to copyItemValues() is all that SdzBag needs to be able to collect together (and copy into a buffer) all the values that have been entered into the rosterslotReferenceDetailNode items. The comment "this property is listened to by the physical NodeController" serves to remind the reader that the implementation of the NodeController acts like any other keeper of javax.swing.AbstractActions: when you set the pasteAction to be enabled then the command will proceed through to whatever form the pasteAction takes, which in our case happens to be that of a javax.swing.JButton on a toolbar. This implementation is determined by the application-housing that is used.

It should be noted that nodes are context-sensitive. As an illustration of this the Copy and Paste buttons only exist on the toolbar when rosterslotReferenceDetailNode is the current node. For the application-housing that Wombat Rescue uses, node switching occurs as the user switches between tabs. Other ways to recognise node switching include menu presses and focus changes.

Every node already has commonly used and well known abilities such as setting the current row, inserting a new DO, removing a DO, entering a query and so on. Other abilities that are unknown by the node, or completely unknown by Strandz, can also be added, but this must be done programmatically rather than via the model. Copy and paste fall into this category. The following code completes our examination of incorporating these new abilities into rosterslotReferenceDetailNode:

List rosterSlotAbilities = new ArrayList();
...
copyPasteButton( rosterSlotAbilities );
dt.rosterslotReferenceDetailNode.setAbilities( rosterSlotAbilities);

Alphabetic Worker selection

The next thing we will look at is how alphabetic Worker selection is incorporated into Wombat Rescue. This time we will start with the setup code. First of all create the AlphabetScrollPane and set it up to display the appropriate letters:

AlphabetScrollPane sp = new AlphabetScrollPane();
simplifiedForDemo = getSimplifiedForDemo();
if(simplifiedForDemo)
{
    String letters[] = { "B", "C", "F", "G", "H", "M", "N", "P", "S", "W"};
    sp.setLetters( letters);
}
else
{
    String letters[] = { "A","B","C","D","E","G","H","J","K","L","Ma","Mu",
                         "N","O","P","R","Sa","Sh","St","T","V","W","Z"};
    sp.setLetters( letters);
}

Then we make the AlphabetScrollPane visually wrap around the first pane of sbI (sbI is a SdzBagI), before putting it in the place of the pane it has just wrapped:

sp.getContentPanel().add( sbI.getPane( 0 ), BorderLayout.CENTER );
sbI.setPane( 0, sp );

Having made sure that the AlphabetScrollPane will be visible to the user we can now create a class to listen to the user's selection:

alphabetListener = new AlphabetActionListener( sbI, dt.workerNode,
  dt.workerCell );
sp.getAlphabetPanel().setActionListener( alphabetListener );

The ActionListener simply goes through all the workerCell's DOs. If it finds a match then the matched Worker is displayed:

/**
 * @param txt - The letter (or sequence of) that you want to do the action for
 */
public void perform(String txt)
{
    int i = 0;
    for(Iterator iter = workerCell.getDataRecords().iterator(); iter.hasNext(); i++)
    {
        Worker vol = (Worker) iter.next();
        if(vol.startsWith(txt))
        {
            break;
        }
    }
    if(i != workerNode.getRowCount())
    {
        sbI.getStrand().SET_ROW(i);
    }
}

Lastly here we make sure that the AlphabetPanel is not accepting button presses while the strand is in ENTER_QUERY state (the state where the values that are entered will shortly be used for a search or a query).

sdzBag.getStrand().addStateChangeTrigger(
    new AlphabetTrigger( sp.getAlphabetPanel()));
...
private static class AlphabetTrigger implements StateChangeTrigger
{
    private AlphabetPanel panel;

    AlphabetTrigger(AlphabetPanel panel)
    {
        this.panel = panel;
    }

    public void stateChangePerformed(StateChangeEvent e)
    {
        StateEnum current = e.getCurrentState();
        StateEnum previous = e.getPreviousState();
        if(current == StateEnum.ENTER_QUERY)
        {
            panel.setEnabled(false);
        }
        else if(previous == StateEnum.ENTER_QUERY)
        {
            panel.setEnabled(true);
        }
    }
}

Data Flow Triggers

Data flow triggers are used to populate application data. Here is how workers are populated:

dt.workerNode.addDataFlowTrigger( new DataFlowT1());
...
class DataFlowT1 implements DataFlowTrigger
{
    public void dataFlowPerformed(DataFlowEvent evt)
    {
        if(evt.getID() == DataFlowEvent.PRE_QUERY)
        {
            dataStore.rollbackTx();
            dataStore.startTx();
            masterQuery();
        }
        else if(evt.getID() == DataFlowEvent.POST_QUERY)
        {// Err.pr( "Have read data in, got " + workerExtent.size() + " Worker records");
        }
    }
}

private void masterQuery()
{
    TimeBandMonitorI lovMonitor = WidgetUtils.getTimeBandMonitor(dataStore.getEstimatedLookupDataDuration());
    TaskI task = new LookupDataTask(dataStore, this); //calls setLOVs()
    lovMonitor.start(task);
    lovMonitor.stop();

    Collection c = null;
    if(title == null)
    {
        Err.error("Not yet called display(), to say which title to query for");
    }
    else if(title == WombatDomainQueryEnum.ROSTERABLE_WORKERS.getDescription())
    {
        c = queriesI.executeRetCollection(WombatDomainQueryEnum.ROSTERABLE_WORKERS);
    }
    else if(title == WombatDomainQueryEnum.UNROSTERABLE_WORKERS.getDescription())
    {
        c = queriesI.executeRetCollection(WombatDomainQueryEnum.UNROSTERABLE_WORKERS);
    }
    else if(title == WombatDomainQueryEnum.GROUP_WORKERS.getDescription())
    {
        c = queriesI.executeRetCollection(WombatDomainQueryEnum.GROUP_WORKERS);
    }
    else
    {
        Err.error("Have not coded for title: <" + title + ">");
    }
    if(c.size() == 0 && title == WombatDomainQueryEnum.ROSTERABLE_WORKERS.getDescription())
    {
        Err.error("Have not found any " + title + " in " + dataStore);
    }

    List enterQryAttribs = dt.workerNode.getEnterQueryAttributes();
    masterList = InterfUtils.refineFromMatchingValues(c, enterQryAttribs);
    dt.workerCell.setData(masterList);
}

The above method is a little bit clunky at the moment because RosterWorkersTriggers is being made to work for what probably ought to be three separate subclassed triggers files. Another thing to note is that this method is designed to be called both when querying as transition from ENTER_QUERY state and when querying as transition from any other state. In the latter case the code will still work because enterQryAttribs will be an empty ArrayList and refineFromMatchingValues() will return the same collection that it is passed.

Also from above LookupDataTask is a monitor for the potentially long running call to setLOVs():

void setLOVs()
{
    List days = queriesI.executeRetList(WombatDomainQueryEnum.ALL_DAY_IN_WEEK);
    List shifts = queriesI.executeRetList(WombatDomainQueryEnum.ALL_WHICH_SHIFT);
    List flexibilities = queriesI.executeRetList(WombatDomainQueryEnum.ALL_FLEXIBILITY);
    List seniorities = queriesI.executeRetList(WombatDomainQueryEnum.ALL_SENIORITY);
    List sexes = queriesI.executeRetList(WombatDomainQueryEnum.ALL_SEX);
    List weeksInMonth = queriesI.executeRetList(WombatDomainQueryEnum.ALL_WEEK_IN_MONTH);
    List intervals = queriesI.executeRetList(WombatDomainQueryEnum.ALL_INTERVAL);
    List overrides = queriesI.executeRetList(WombatDomainQueryEnum.ALL_OVERRIDE);
    List monthsInYear = queriesI.executeRetList(WombatDomainQueryEnum.ALL_MONTH_IN_YEAR);
    Collection groups = queriesI.executeRetCollection(WombatDomainQueryEnum.WORKER_GROUPS);
    List pickGroups = new ArrayList(groups);
    pickGroups.add(0, queriesI.executeRetObject(WombatDomainQueryEnum.NULL_WORKER));

    dt.dayInWeekLookupCell.setLOV(days);
    dt.whichShiftLookupCell.setLOV(shifts);
    dt.shiftPreferenceLookupCell.setLOV(shifts);
    dt.flexibilityLookupCell.setLOV(flexibilities);
    dt.seniorityLookupCell.setLOV(seniorities);
    dt.sexLookupCell.setLOV(sexes);
    dt.belongsToGroupLookupCell.setLOV(pickGroups);
    dt.weekInMonthLookupCell.setLOV(weeksInMonth);
    dt.intervalLookupCell.setLOV(intervals);
    dt.overridesOthersLookupCell.setLOV(overrides);
    dt.onlyInMonthLookupCell.setLOV(monthsInYear);
    dt.notInMonthLookupCell.setLOV(monthsInYear);
}

The setLOV() method gives the cell (and hence the user) a range of values from which to select how the lookup will be 'plugged'. Note that if the user does not specifically select a lookup DO that the first in the list will be used. Certain controls are specified as being 'lookup capable', and thus are able to represent these lists to the user.

The detail query is simpler:

class DataFlowT2 implements DataFlowTrigger
{
    public void dataFlowPerformed(DataFlowEvent evt)
    {
        if(evt.getID() == DataFlowEvent.PRE_QUERY)
        {
            detailQuery();
        }
    }
}

private void detailQuery()
{
    detailList = queriesI.executeRetList(WombatDomainQueryEnum.ALL_ROSTER_SLOT);
    dt.rosterslotReferenceDetailCell.setData(detailList);
}

The reason that a detail data flow trigger needs to be written at all is that the master-detail relationship between Worker and RosterSlot is based on a reference within RosterSlot to Worker, rather than each Worker having its own collection of RosterSlots.

Record Validation


We will leave you to continue the examination of this source code. Hopefully we have covered everything that might need explantion ...