Input validation and messaging in the Process Guide Framework

Input validation and messaging in the Process Guide Framework

A few words of introduction: Process Guide framework is THE framework for new Warehouse management mobile screens and menu items in “Dynamics 365 Supply Chain Management”:
…/warehousing/process-guide-framework

I find that the topic of the validation of data entered or scanned by the user is very superficially covered. When you read the above shallow documentation you may think the ProcessGuideStep.isComplete() method is a good place to give an error, but it is NOT. It would be too late: by the time the execution reaches isComplete() the user input has already been processed, accepted and serialized in the session state aka “pass” WHSRFPassthrough.

I figured out that the true validation may be programmed at 2 places: in the process() method of a class derived from the WHSControl or in the ProcessGuideStep.validateControls() method. The 2 ways require slightly different techniques and have distinct flavours:

WHSControl.process()

Inheriting the WHSControl class offers an object-oriented alternative to extending the WHSRFControlData class (this spaghetti code has been recently refactored and moved to WHSRFControlData.processLegacyControl). This technique is briefly described in the blog …/mfp/posts/extending-whs-adding-a-new-control-type .
There are pros and cons. The class is going to be used across the whole warehouse mobile app whenever you add a control with this name. This can be both good and bad:

  • The major disadvantage is that the WHSControl.process() is not triggered if the user has not entered anything into the field on the mobile device. E.g. one cannot check for an empty value, and this may lead to the “xxx is not found in the map” error message if one does not check existence of a parameter before fetching it from the “pass” WHSRFPassthrough.
  • The same validation logic is used everywhere. The code re-use is innate. This can be however too rough; for example, the below code snippet was written because the standard logic of the ProdID control was too broad: it was populating the item number and validating if it had a BOM.
  • The control is not fully aware of the context. It cannot reach the current Process Guide step object and call the Process Guide controller object back.
  • It is not fully aware of the surrounding controls either: the controls placed on the page after the current one have left no trace yet.
  • The control class can leverage the WHSRControlData instance with all its perks and shortcuts.

In WHSControl.process() you take the user entry from the parmData() and return a false result with the this.fail() method:
#WHSRF [WhsControlFactory(#ProdIdLabel)]
public class WHSControlProdId extends WhsControl
{
public boolean process()
{
boolean ok = super();
ProdId prodId = this.parmData();
fieldValues.insert(ProcessGuideDataTypeNames::ProdId, prodId);
if (! ProdTable::exist(prodId))
{
ok = this.fail("@WAX1162"); // The production or batch order number is not valid
}
return ok;
}
}

Note that you have to care about parsing the entry and storing it in the fieldValues container. On an error, the processing stops, the page reloads and the error message(s) is shown:
WHS warning

ProcessGuideStep.validateControls()

The validation in the ProcessGuideStep class has a different quality:

  • One can check for empty (mandatory) data.
  • The logic can only be re-used in a menu item made in the Process Guide framework. Such menu items are too few. You re-use the logic by adding the whole step = screen to the process flow.
  • The step has a full access to the process controller and to the preceding step results.
  • All controls are validated at once and can be checked for mutual consistency.
  • The WHSRControlData instance with all its useful methods has already been disposed of by the time execution reaches the validateControls() method. You have to write most of the business logic from scratch.

The validateControls() method is executed before the fieldValues container is merged with the “pass” container (WHSRFPassthrough) and becomes available to the mobile application. This is why you take the user entry safely from the processingResult.fieldValues structure. If you encounter an error, you throw a warning:
[ProcessGuideStepName(classStr(ProcessGuideItemConsumpPromptQtyStep))]
class ProcessGuideItemConsumpPromptQtyStep extends ProcessGuideStep
{
protected void validateControls()
{
WhsrfPassthrough pass = controller.parmSessionState().parmPass();
super();
qty = processingResult.fieldValues.lookup(ProcessGuideDataTypeNames::Qty);
if (qty * pass.parmQty() < 0)
{
throw warning("@SYS25506"); // Quantity may not change sign
}
}
protected ProcessGuidePageBuilderName pageBuilderName()
{
return classStr(ProcessGuideItemConsumpPromptQtyPageBuilder);
}
}

The warning thrown interrupts the flow, re-loads the page and presents the warning.

Success message

A related topic is an ability to present a success message to the user at the end of the process execution. You do it at the end of the doExecute() method in the process guide step:
protected void doExecute()
{
super();
// do the work
this.addProcessCompletionMessage();
}

This implementation is very limited, however: it cannot uptake a custom message and always shows the same text “Work Completed”.

The below snippet is smarter:
ProcessGuideMessageData messageData = ProcessGuideMessageData::construct();
messageData.message = strFmt("@SYS24802", journalId); // Journal cannot be posted because it contains errors.
messageData.level = WHSRFColorText::Warning;
navigationParametersFrom = navigationParametersFrom ? navigationParametersFrom : ProcessGuideNavigationParameters::construct();
navigationParametersFrom.messageData = messageData;

Unrelated conclusion: use the process guide framework and say goodbye to the WHSWorkExecute mess! 🙂