0
votes

I have a large 'order form' XPage that displays 99 rows, with 3 text input boxes on each row. To capture changes, I have placed a call to a SSJS function in the 'onchange' event of each input box. The call simply sends the product ID, the type of change (which column) and the quantity. The SSJS function then preserves those changes in a sessionScope variable (java.util.HashMap). There is no refresh associated with the change.

The changes are processed en masse when the user clicks the 'Submit' button. That is another SSJS function that simply writes all of the changes to the back-end Domino database.

That all seems to work fine and has done for a couple of years. However, it seems my users are becoming too efficient with the application, and are typing faster than it can keep up.

My debug code writes each change to the server's console, and I can see where some changes are simply ignored if the user makes changes in quick succession (they simply tab between the input boxes). It's almost as if the server is too busy processing the previous change and skips one to move on to another. At times, whole blocks of changes are missed, and then the application picks back up when it can.

Am I using the wrong technique to capture the changes? Is there something I can do to ensure the application initiates the onchange event each and every time?

I have tested this using IE8/9 & FF24. I have looked at other such posts that propose using the 'onkeyup' event instead. I don't think that would work in my case, as the users may order double-digit quantities.

Any/all suggestions would be gratefully appreciated!

1
Only one event can run at a time, so if an event is triggered while a previous event is still pending, it'll block the new event automatically. Since nothing happens with this data until "Submit", why trigger a separate change event for each component?Tim Tripcony
Thanks for your reply Tim! Sorry, I missed a key part of my explanation... I need to store the changes made by users prior to the order submission because there are multiple pages on the order form (not just the initial 99 rows). By storing each individual change in the sessionScope variable, I am able to submit them altogether at the point which the user clicks the submission button.Terry Boyd
Sorry, just one more point to support my reason for 'batch processing' all the changes at the time of submission - Business logic dictates that all of the changes be processed together. Purchasing rule logic (multi-item purchase discounts, items that must be purchased together, etc) is contained in a LotusScript library shared with Notes clients, and that library relies on a complete batch of submitted changes.Terry Boyd
Actually, I'm not trying to convince you to forgo the batch processing; rather, forgo the individual processing. Your existing events place information in the session scope... instead, bind the whole interface (or, at least, the repeat) to an object in scope, and when "Submit" is clicked, your scope is updated automatically, no matter how many rows are involved. That's basically what Stephan's answer is illustrating.Tim Tripcony
Thanks Tim - I getchya. I've taken Stephan's comments/code onboard and have begun learning/developing a managed bean solution. Really appreciate you taking the time to explain, and looking forward to buying y'all a drink in Orlando!Terry Boyd

1 Answers

3
votes

Terry, you need to revisit the architecture. If the updates are processed on submit, why bother to send them individually to the server - as Tim nicely pointed out. What I would do:

  • create 2 Java classes: one "Order" one "LineItem"
  • Let the Order class implement the Map interface Map
  • Use the Order class for your repeat control (it will give you the key of each LineItem as the repeat variable)
  • Bind the fields inside the repeat to Order[RepeatKey].fieldName
  • Use Order in a object data source
  • Implement the save method in the Order class and call it in the save method of the object data source

Very rought outline, let me know if you need me to elaborate. The Java Collections Framework is your friend.

It is easier than it looks:

   public class LineItem {

private String unid;
private String partno;
private int quantity;
private long unitprice;

/**
 * Constructor for new items
 */
public LineItem() {
    this.unid = null;
}

/**
 * Constructor for existing items
 */
public LineItem(Document doc) {
    this.unid = doc.getUniversalId();
    // more here
}


/**
 * @return the unid
 */
public String getUnid() {
    return this.unid;
}

/**
 * @return the partno
 */
public String getPartno() {
    return this.partno;
}
/**
 * @param partno the partno to set
 */
public void setPartno(String partno) {
    this.partno = partno;
}
/**
 * @return the quantity
 */
public int getQuantity() {
    return this.quantity;
}
/**
 * @param quantity the quantity to set
 */
public void setQuantity(int quantity) {
    this.quantity = quantity;
}
/**
 * @return the unitprice
 */
public long getUnitprice() {
    return this.unitprice;
}
/**
 * @param unitprice the unitprice to set
 */
public void setUnitprice(long unitprice) {
    this.unitprice = unitprice;
}

public void save(Database db) {
    Document doc = null;
    if (this.unid == null) {
        doc = db.createDocument();
        doc.replaceItem("Form", "LineItem");
    }
    doc.replaceItem("PartNo", this.partno);
    // More here
    doc.save();
}
}

and for the Order - presuming you load from a document collection.

public class Order implements Map<String, LineItem> {

// You might want to have a stack here to keep order
private final Map<String, LineItem> backingMap          = new LinkedHashMap<String, LineItem>();
private final Set<String>           deletedItemKeys     = new HashSet<String>();

// The key we use for new items when unid is null
private int                         lastNewItemNumber   = 0;

@Override
public int size() {
    return this.backingMap.size();
}

@Override
public boolean isEmpty() {
    return this.backingMap.isEmpty();
}

@Override
public boolean containsKey(Object key) {
    return this.backingMap.containsKey(key);
}

@Override
public boolean containsValue(Object value) {
    return this.backingMap.containsValue(value);
}

@Override
public LineItem get(Object key) {
    return this.backingMap.get(key);
}

@Override
public LineItem put(String key, LineItem value) {
    // Here it gets a little special
    // We need to prevent null keys
    if (key == null) {
        key = String.valueOf(this.lastNewItemNumber);
        lastNewItemNumber++;
    }
    this.deletedItemKeys.remove(key);
    return this.backingMap.put(key, value);
}

@Override
public LineItem remove(Object key) {
    this.deletedItemKeys.add(key.toString());
    return this.backingMap.remove(key);
}

@Override
public void putAll(Map<? extends String, ? extends LineItem> m) {
    for (Map.Entry<? extends String, ? extends LineItem> me : m.entrySet()) {
        this.put(me.getKey(), me.getValue());
    }
}

@Override
public void clear() {
    this.deletedItemKeys.addAll(this.backingMap.keySet());
    this.backingMap.clear();
}

@Override
public Set<String> keySet() {
    return this.backingMap.keySet();
}

@Override
public Collection<LineItem> values() {
    return this.backingMap.values();
}

@Override
public Set<java.util.Map.Entry<String, LineItem>> entrySet() {
    return this.backingMap.entrySet();
}

public void load(NotesDocumentCollection dc) throws NotesException {
    Document doc = dc.getFirstDocument();
    Document nextDoc;
    while (doc != null) {
        nextDoc = dc.getNextDocument(doc);
        LineItem li = new LineItem(doc);
        this.put(doc.getUniversalId(), li);
        doc.recycle();
        doc = nextDoc;
    }

    doc.recyle();
}

public void save(Database db) {
    for (LineItem item : this.backingMap.values()) {
        item.save(db);
    }

    // Now kill the left overs - needs error handling
    for (String morituri : this.deletedItemKeys) {
        Document delDoc = db.getDocumentByUnid(morituri);
        if (delDoc != null) {
            delDoc.remove(true);
        }
    }       
}
}