Customizing the Warehousing Mobile App
Introduction
We last looked at the Warehouse Mobile Devices Portal (WMDP) in detail in a series of blog posts here, here, and here. The last one covered how to build custom solutions and walked through building a new sample workflow for the WMDP. This post will be updating that sample to cover some of the changes that have occurred with the Advanced Warehousing solution and the Dynamics 365 for Finance and Operations – Enterprise Edition warehousing application.
WMDP vs Dynamics 365 for Warehousing Mobile App
The Warehouse Mobile Devices Portal (WMDP) interface, which is an IIS-based HTML solution (described in detail here), is being deprecated in the July 2017 release of Dynamics 365 for Finance and Operations (see deprecated features list here). Replacing this is a native mobile application shipping on Android and Windows 10 devices. The mobile app is a complete replacement for the WMDP and contains a superset of capabilities – all existing workflows available in the WMDP will operate in the new mobile app. You can find more detail on the mobile app here and here.
Customizing the new Dynamics 365 for Warehousing Mobile App
The process for customizing the new mobile app is largely unchanged – you can still utilize the X++ class hierarchy discussed in the previous blog post. However – I want to walk through some of the differences that enable customizations to exist as purely extensions. The previous solution required a small set of overlayered code. Moving forward this practice is being discouraged and we recommend all partners and customers create extensions for any customizations.
As before, we will be focusing on building a new workflow around scanning and weighing a container. The inherent design concept behind the Advanced Warehousing solution is unchanged – you will still need to think and design these screens in terms of a state machine – with clear transitions between the states. The definition of what we will build looks like this:
WHSWorkExecuteMode and WHSWorkActivity Enumerations
Just as in the previous blog post – to add a new “indirect work mode” workflow we will need to add values to the two enumerations WHSWorkExecuteMode and WHSWorkActvity. The new enum names need to match exactly as one will be used to instantiate the other deep inside the framework. Note that both should be added as enumeration extensions built in a custom model. Once this has been done it will be possible to create the menu item in the UI – since the WHSWorkActvity enumeration controls the list of available workflows in the UI:
You can see the extension enumeration values in the following screenshots:
WHSWorkExecuteDisplay class
The core logic will exist within a new class you will create, which will be derived from the WhsWorkExecuteDisplay base class. This class is largely defined the same way as the WMDP-based example, however there is now a much easier way to introduce the mapping between the Execute Mode defined in the Menu Item and the actual class which performs the workflow logic – we can use attributes to map the two together. This also alleviates the need to overlay the base WHSWorkExecuteDisplay class to add support for new derived classes (as the previous WHSWorkExecuteDisplay “factory method” construct forced us to do).
The new class will be defined like this:
[WHSWorkExecuteMode(WHSWorkExecuteMode::WeighContainer)] class conWhsWorkExecuteDisplayContainerWeight extends WhsWorkExecuteDisplay { }
Note that all the new classes I am adding in this example will be prefixed with the “con” prefix (for Contoso). Since there is still no namespace support it is expected partner code will still leverage this naming scheme to minimize naming conflicts moving forward.
The displayForm method is required – and acts as the primary entry point to the state machine based workflow. This is completely unchanged from the previous example:
[WHSWorkExecuteMode(WHSWorkExecuteMode::WeighContainer)] class conWhsWorkExecuteDisplayContainerWeight extends WhsWorkExecuteDisplay { container displayForm(container _con, str _buttonClicked = '') { container ret = connull(); container con = _con; pass = WHSRFPassthrough::create(conPeek(_con, #PassthroughInfo)); if (this.hasError(_con)) { con = conDel(con, #ControlsStart, 1); } switch (step) { case conWeighContainerStep::ScanContainerId: ret = this.getContainerStep(ret); break; case conWeighContainerStep::EnterWeight: ret = this.getWeightStep(ret, con); break; case conWeighContainerStep::ProcessWeight: ret = this.processWeightStep(ret, con); break; default: break; } ret = this.updateModeStepPass(ret, WHSWorkExecuteMode::WeighContainer, step, pass); return ret; } }
A detailed analysis of this code can be found in the previous blog post – we will skip forward to the definition of the getContainerStep method, which is where the first screen is defined. The two methods used to define the first screen are below:
private container getContainerStep(container _ret) { _ret = this.buildGetContainerId(_ret); step = conWeighContainerStep::EnterWeight; return _ret; } container buildGetContainerId(container _con) { container ret = _con; ret += [this.buildControl(#RFLabel, #Scan, 'Scan a container', 1, '', #WHSRFUndefinedDataType, '', 0)]; ret += [this.buildControl(#RFText, conWHSControls::ContainerId, "@WAX1422", 1, pass.lookupStr(conWHSControls::ContainerId), extendedTypeNum(WHSContainerId), '', 0)]; ret += [this.buildControl(#RFButton, #RFOK, "@SYS5473", 1, '', #WHSRFUndefinedDataType, '', 1)]; ret += [this.buildControl(#RFButton, #RFCancel, "@SYS50163", 1, '', #WHSRFUndefinedDataType, '', 0)]; return ret; }
Note that I am using a class to define any custom constants required for the Warehousing logic. This was typically done with macros in the previous version – but these can cause some issues in extension scenarios. So instead we are encouraging partners to define a simple class that can group all their constants together – which can then be referenced as you see in the code above. The only area where this does not work is in attribute definitions – this will still need a Macro or String definition. Here is mine so far for this project:
class conWHSControls { public static const str ContainerId = "ContainerId"; public static const str Weight = "Weight"; }
The other important thing to notice in the above code is that I have explicitly defined the data type of the input field (in this case extendedTypeNum(WHSContainerId)). This is important as it tells the framework exactly what type of input field to construct – which brings us to the new classes you need to add to support the new app functionality.
New Fields
In the previous version of this blog we discussed the fact that since we are adding new fields to the warehousing flows that are not previously handled in the framework we must modify (i.e. overlayer) some code in the WHSRFControlData::processControl method. This allows the framework to understand how to handle the ContainerId and Weight fields when they are processed by the WMDP framework.
In the new model these features are controlled through two new base classes to customize and manage the properties of fields. The WHSField class defines the display properties of the field in the mobile app – and it is where the default input mode and display priorities are extracted when the user configures the system using the process described here. The WhsControl class defines the logic necessary for processing the data into the field values collection. For my sample, we need to add support for the ContainerId field – so I have added the following two new classes:
[WhsControlFactory('ContainerId')] class conWhsControlContainerId extends WhsControl { public boolean process() { if (!super()) { return false; } fieldValues.insert(conWHSControls::ContainerId, this.data); return true; } } [WHSFieldEDT(extendedTypeStr(WHSContainerId))] class conWHSFieldContainerId extends WHSField { private const WHSFieldClassName Name = "@WAX1422"; private const WHSFieldDisplayPriority Priority = 65; private const WHSFieldDisplayPriority SubPriority = 10; private const WHSFieldInputMode InputMode = WHSFieldInputMode::Scanning; private const WHSFieldInputType InputType = WHSFieldInputType::Alpha; protected void initValues() { this.defaultName = Name; this.defaultPriority = Priority; this.defaultSubPriority = SubPriority; this.defaultInputMode = InputMode; this.defaultInputType = InputType; } }
Obviously my conWhsControlContainerId class is not doing much – it is just taking the data from the control and placing it into the fieldValues map with the ContainerId name – which is how I will look for the data and utilize it later in the system. If there was more complex validation or mapping logic I could place that here. For example, the following is a snapshot of the process logic in the WhsControlQty class – this manages the logic for entering in quantity values from the mobile app:
public boolean process() { Qty qty = WHSWorkExecuteDisplay::str2numDisplay(data); if (qty <= 0) { return this.fail("@WAX1172"); } if (mode == WHSWorkExecuteMode::Movement && WHSRFMenuItemTable::find(pass.lookup(#MenuItem)).RFDisplayStatus) { controlData.parmFromInventStatusId(controlData.parmInventoryStatusSelectedOnControl()); } else { controlData.parmFromInventStatusId(controlData.getInventStatusId()); } if (!super()) { return false; } if (mode == WHSWorkExecuteMode::Movement && fieldValues.exists(#Qty)) { pass.parmQty(qty ? data : ''); } else { fieldValues.parmQty(qty ? data : ''); } //When 'Display inventory status' flag is unchecked, need the logic for #FromInventoryStatus and #InventoryStatusId this.populateDataForMovementByTemplate(); return true; }
The buildGetWeight method is very similar to the previous UI method – the only real difference is the Weight input data field. Note that we don’t need to define a custom WHSField class for this field because it already exists in the July Release.
Error Display
There was another minor change that was necessary before I could get the expected behavior, and it points to a slight change in the framework itself. In the previous version of the code when I reported that the weight was successfully saved I did so with an “addErrorLabel” call and passed in the WHSRFColorText::Error parameter to display the message at the top of the screen. This same code in the new warehousing app will now cause the previous step to be repeated, meaning I will not get the state machine transition I expect. Instead I need to use the WHSRFColorText::Success parameter to indicate that I want to display a status message but it should not be construed as an error condition.
container processWeightStep(container _ret, container _con) … ttsBegin; containerTable = WHSContainerTable::findByContainerId(pass.lookupStr(conWHSControls::ContainerId),true); if(containerTable) { containerTable.Weight = pass.lookupNum(conWHSControls::Weight); containerTable.update(); _ret = conNull(); _ret = this.addErrorLabel(_ret, 'Weight saved', WHSRFColorText::Success); pass.remove(conWHSControls::ContainerId); _ret = this.getContainerStep(_ret); } else { _ret = conNull(); _ret = this.addErrorLabel(_ret, 'Invalid ContainerId', WHSRFColorText::Error); pass.remove(conWHSControls::ContainerId); _ret = this.getContainerStep(_ret); } ttsCommit;
Caching
The mobile app as well as the AOS perform a significant amount of caching, which can sometimes make it difficult to add new classes into the framework. This is because the WHS code is heavily leveraging the SysExtension framework. I find that having a runnable class included in the project which simply calls the SysExtensionCache::clearAllScopes() method can help resolve some of these issues.
Conclusion
At this point I have a fully functional custom workflow that will display the new fields correctly in the mobile app. You can see the container input field and weight input below. Note that if you want to have the weight field display the “scanning” interface you can change the “preferred input mode” for the Weight EDT on the “Warehouse app field names” screen within the Dynamics 365 environment itself.
The Dynamics 365 for Operations project for this can be downloaded here. This code is provided “as-is” and is meant only as a teaching sample and not meant to be used in production environments. Note that the extension capabilities described in this blog are only available in the July 2017 release of Dynamics 365 for Finance and Operations or later.