Month: May 2020

Building Warehouse Applications with the ProcessGuide API

In my previous posts I have showed how to build custom workflows for the Warehouse mobile app in Dynamics.  All my previous examples have used the standard WHSWorkExecuteDisplay class framework.  One of the common complaints about this framework is the difficulty in which partners have with extending existing functionality or workflows.  To address these extensibility challenges the product team refactored the core API into a new framework called ProcessGuide which provides more opportunities to extend functionality at different points in the lifecycle of a warehouse workflow.  The full details of this change can be found here: https://docs.microsoft.com/en-us/dynamics365/supply-chain/warehousing/process-guide-framework

I would like to walk through the process of creating a new workflow in the ProcessGuide framework.  I will revisit my previous example and show how we would accomplish the same outcome, but this time utilizing the ProcessGuide framework.

The requirement is that we want a user to scan a set of assets against a single container within the warehouse.  These assets might be serial number controlled items or unique catch weight tags that you need to track within the system.  This article (https://cloudblogs.microsoft.com/dynamics365/it/2018/12/20/customizing-the-warehouse-mobile-app-multi-scan-pages/) discussed how we can use the offline multi-scan controller pattern to allow the user to scan these assets without having to do a round trip to the server with every scan.  Customers are using this pattern to enable high performance scanning workflows.

Process Guide Classes

The key objects to create in the ProcessGuide framework are documented in the above article, but I will copy them here for reference:

  • ProcessGuideController– This class orchestrates the overall execution of the business process. It defines the factories that instantiate the step and the navigation agent, which subsequently constitute the process execution, as well as the clean-up logic for cancellation or exiting the process.
  • ProcessGuideStep– This class represents one single step in the business process. This class contains a definition of the factories that instantiate a page builder, actions, and data processors and is responsible for invoking them in the correct sequence.
  • ProcessGuideNavigationAgent– This class is responsible for navigation between the steps. When a step is completed, the navigation agent is responsible for defining the next step and passes any parameters that the previous step may need to communicate to the next one.
  • ProcessGuidePageBuilder– This class is responsible for instantiating the user interface.
  • ProcessGuideAction– This class represents an action, shown as a button to the user.
  • ProcessGuideDataProcessor– This class is responsible for processing the user entered data in a field.

This can be somewhat hard to visualize based on the above descriptions – so I have tried to represent the major objects below.  What is key to see here is that when you create a new workflow you will have 1 or more ProcessGuideStep classes, and each one will have to define a ProcessGuidePageBuilder if they present a UI to the user.  It is possible to have a processing step with no UI, in which case there will be no PageBuilder class defined – we will see an example of this below.  The ProcessGuideNavigationAgent will define the transitions (or if you want to be all computer science-y about it – the edges in the directed graph) between the steps.

WHSWorkActivity and WHSWorkExecuteMode Enumerations

These two key enumerations still drive the core sysExtension Framework mapping – so one of the first steps is to define the new enumerations as an extension on the existing enumerations.  This process is unchanged from the previous articles.

ProcessGuideController

The class that defines the overall state-machine logic in the ProcessGuide framework is the ProcessGuideController.  This class will determine the discrete steps and conceptually maps to the high-level WHSWorkExecuteDisplay<ProcessName> class you might have defined previously.  When you create a custom workflow you will first define this class – however you will need to define the several other classes before you can finish the definition of the class itself.  For now, we will define the code like this:

[WHSWorkExecuteMode(WHSWorkExecuteMode::AssetScan)]
public class WHSProcessGuideAssetScanController extends ProcessGuideController
{
    protected ProcessGuideStepName initialStepName()
    {
        return null;
    }

    protected ProcessGuideNavigationRoute initializeNavigationRoute()
    {
        return null;
    }
}

Note that we are associating the Controller class with our new WHSWorkExecuteMode enumeration value in the class attribute so this controller class will be instantiated when the mobile app constructs these type of menu items.

As discussed above the Controller defines the navigation steps between the various state machine “steps.”  These steps are defined as classes derived from the ProcessGuideStep class.  Based on our workflow diagram above, we will have three steps in this workflow – one for each of the “states” (Scan ContainerID, Capture Assets, and Process Assets).

Scan ContainerID

We can define the first ProcessGuideStep with the following structure:

[ProcessGuideStepName(classStr(WHSProcessGuideValidateAssetScanContainerIdStep))]
public class WHSProcessGuideValidateAssetScanContainerIdStep extends ProcessGuideStep
{
    protected ProcessGuidePageBuilderName pageBuilderName()
    {
        return null;
    }
}

The pageBuilderName is required – this is where you will define the specific PageBuilder used to render this step. A PageBuilder class roughly corresponds to the displayForm method in the WHSWorkExecuteDisplay framework.  In the previous article this was the code used to construct this step (via the WHSWorkExecute framework):

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;
}

The equivalent code in the ProcessGuide framework is below.  Note that you are constructing a ProcessGuidePageBuilder class and the data controls and action controls are split into separate methods (which allows for better extensibility).

[ProcessGuidePageBuilderName(classStr(WHSProcessGuideValidateAssetScanContainerIdPageBuilder))]
public class WHSProcessGuideValidateAssetScanContainerIdPageBuilder extends ProcessGuidePageBuilder
{
    protected final void addDataControls(ProcessGuidePage _page)
    {
        _page.addLabel(ProcessGuideDataTypeNames::ContainerIdLabelName, "Scan a container", extendedTypeNum(WHSRFUndefinedDataType));
        _page.addTextBox(ProcessGuideDataTypeNames::ContainerId, "@WAX1422", extendedTypeNum(WHSContainerId));
    }

    protected final void addActionControls(ProcessGuidePage _page)
    {
        #ProcessGuideActionNames
        _page.addButton(step.createAction(#ActionOK), true);
        _page.addButton(step.createAction(#ActionCancelExitProcess));
    }
}

Now that we have defined the PageBuilder class we can go back and update the Step class with the necessary details.  Specifically, the pageBuilderName method can be updated to return the class we just defined.  In addition, we need to define criteria for when this step is considered “complete” – this is done by overriding the isComplete method.  For this example we will require the ContainerId textbox to have data in it.

[ProcessGuideStepName(classStr(WHSProcessGuideValidateAssetScanContainerIdStep))]
public class WHSProcessGuideValidateAssetScanContainerIdStep extends ProcessGuideStep
{
    protected final boolean isComplete()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        WHSContainerId containerId = pass.lookup(ProcessGuideDataTypeNames::ContainerId);
        return (containerId != '');
    }

    protected ProcessGuidePageBuilderName pageBuilderName()
    {
        return classStr(WHSProcessGuideValidateAssetScanContainerIdPageBuilder);
    }
}

Control Names

Note that I am using the ProcessGuideDataTypeNames class to define all the control names – this can be easily done with an extension class like below.  It also could be defined in another class – this is not a hard requirement; it just matches what the core ProcessGuide API is doing.

[ExtensionOf(classStr(ProcessGuideDataTypeNames))]
final static class ProcessGuideDataTypeNamesWHSTestPG_Extension
{
    public static const str ContainerId = "ContainerId";
    public static const str ContainerIdLabelName = "ContainerIdLabel";
    public static const str AssetId = "AssetId";
}

Capture Assets

We can now build the next step in the workflow – which means we need both a ProcessGuideStep and ProcessGuidePageBuilder class.  Since this step will be using the multi-scan pattern there will be one small addition to these two classes. The page builder will be defined in a similar manner to the previous class:

[ProcessGuidePageBuilderName(classStr(WHSProcessGuideAssetScanMultiScanPageBuilder))]
public class WHSProcessGuideAssetScanMultiScanPageBuilder extends ProcessGuidePageBuilder
{
    protected void addDataControls(ProcessGuidePage _page)
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        str assetIdList = pass.lookupStr(ProcessGuideDataTypeNames::AssetId);
        
        _page.addLabel(ProcessGuideDataTypeNames::ContainerIdLabelName,"Scan Assets", extendedTypeNum(WHSRFUndefinedDataType));
        _page.addTextBox(ProcessGuideDataTypeNames::AssetId, "Asset Id", extendedTypeNum(AssetIdList), true, assetIdList);
    }

    protected void addActionControls(ProcessGuidePage _page)
    {
        #ProcessGuideActionNames
        _page.addButton(step.createAction(#ActionOK), true);
        _page.addButton(step.createAction(#ActionCancelResetProcess));
    }

}

And the ProcessGuideStep will simply indicate that the specific PageBuilder class should be used:

[ProcessGuideStepName(classStr(WHSProcessGuideAssetScanMultiScanStep))]
public class WHSProcessGuideAssetScanMultiScanStep extends ProcessGuideStep 
{
    protected final ProcessGuidePageBuilderName pageBuilderName()
    {
        return classStr(WHSProcessGuideAssetScanMultiScanPageBuilder);
    }
}

What makes this step unique is the client-side multi-scan mode that is enabled for the user.  This allows the customer to scan any number of Assets in an offline mode and not have to wait for the server-side round trip for each scan.  In order to tell the mobile app that we want to be in this “multi-scan” mode we need to create our own DectoratorFactory object and return the Multi-Scan decorator for this specific step.  This will work the same way it did in the previous example; we will define a new MultiScan DecoratorFactory and have it return a new multi-scan WHSMobileAppServiceXMLDecorator at the specific step in the workflow we want the user to be in the multi-scan UI.  Recall that the WHSMobileAppServiceXMLDecorator class is used to direct the mobile app UI to change into the different major “modalities” – such as the standard scan control UI, the card view used for the worklist, and the multi-scan model.

We still do not ship a native MultiScan decorator factory, but it can be easily created for this specific flow with the following code.  This detects which step we are currently executing and if we are on the multi-scan step it will return the multi-scan decorator.

[WHSWorkExecuteMode(WHSWorkExecuteMode::AssetScan)]
public class WHSMobileAppServiceXMLDecoratorFactoryMultiScan implements WHSIMobileAppServiceXMLDecoratorFactory
{
    #WHSRF

    public WHSMobileAppServiceXMLDecorator getDecorator(container _con)
    {
        if (this.isMultiScanScreen(_con))
        {
            return new WHSMobileAppServiceXMLDecoratorMultiScan();
        }
        
        return new WHSMobileAppServiceXMLDecoratorFactoryDefault().getDecorator(_con);
    }

    /// <summary>
    /// Extracts the current step from the container.
    /// the current step should be preceeded by a string "CurrentStep"
    /// </summary>
    /// <param name = "_con">
    /// Contains information about the context
    /// </param>
    /// <returns>
    /// The current step listed in the context
    /// </returns>
    private str getCurrentStep(container _con)
    {
        container subCon = conPeek(_con, 2);
        for (int i  = 1; i <= conLen(subCon); i++)
        {
            if (conPeek(subCon, i - 1) == "CurrentStep")
            {
                return conPeek(subCon, i);
            }
        }

        //Default behavior.
        return conPeek(subCon, 8);
    }

    private boolean isMultiScanScreen(container _con)
    {
        const str MultiStep = classstr(WHSProcessGuideAssetScanMultiScanStep);

        str currStep = this.getCurrentStep(_con);

        return currStep == MultiStep;
    }

}

Process Assets

The final step in the workflow is to take the data entered by the user and submit it to the database.  For this we don’t need to display a UI, so we will create a step that derives from ProcessGuideStepWithoutPrompt.  This means we just need to provide an implementation for the doExecute method.  We will leverage the same code from the previous article – as a reminder here is the overview of the API used by the multi-scan page pattern API.


Multi-Scan API

Now that we know how to enable the Multi-Scan UI through a Page Pattern, we need to understand the basic API for passing the scanned items back and forth.  Once the MultiScan Page Pattern is requested, the first input control registered on the page will be used for the multi-scan input.  Remember that most of the UI interaction is all done client-side – so the only thing the server X++ code needs to do is define this control and the data that it contains.

When the user clicks that “submit” check box and sends the multi-scan data back to the X++ code, this is formatted in a very specific way.  The actual parsing of the data is done using the same interaction patterns as before – it will be stored in the result pass object for the specific control defined as the primary input of this page.  But the data will be passed in this format:

   <scanned value>, <number of scans>|<scanned value>, <number of scans>|…

Thus, in my demo example above the data that the server would receive would be the following:

   BC-001,2|BC-002,1|BC-003,1

In the X++ code you would then be responsible for parsing this string and storing the data in the necessary constructs.  We will see a simple example in a moment of how to parse this data.


The following is the code necessary to build the processing step:

[ProcessGuideStepName(classStr(WHSProcessGuideAssetScanProcessAssetsStep))]
public class WHSProcessGuideAssetScanProcessAssetsStep extends ProcessGuideStepWithoutPrompt
{
    protected final void doExecute()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        
        if(pass.lookupStr(ProcessGuideDataTypeNames::AssetId) == "")
        {
            throw error('No assets found');
        }
        else
        {
            List assets = strSplit(pass.lookupStr(ProcessGuideDataTypeNames::AssetId),"|");
            ListEnumerator enumerator = assets.getEnumerator();
            while(enumerator.moveNext())
            {
                //save assetId
                str assetString = enumerator.current();
                if (assetString != "")
                {
                    str AssetId = subStr(assetString,1,strScan(assetString,",",1,strLen(assetString))-1 );
                    WHSAsset newAsset;
                    newAsset.ContainerId = pass.lookupStr(ProcessGuideDataTypeNames::ContainerId);
                    newAsset.WHSAssetId = AssetId;
                    newAsset.insert();
                }
            }
             this.addAssetsSavedProcessCompletionMessage();     
        }

        super();

        pass.remove(ProcessGuideDataTypeNames::ContainerId);
        pass.remove(ProcessGuideDataTypeNames::AssetId);
    }

    private void addAssetsSavedProcessCompletionMessage()
    {
        ProcessGuideMessageData messageData = ProcessGuideMessageData::construct();
        messageData.message = "Assets Saved";
        messageData.level = WHSRFColorText::Success;

        navigationParametersFrom = ProcessGuideNavigationParameters::construct();
        navigationParametersFrom.messageData = messageData;
    }

}

ProcessGuideController Finale

We now have all the pieces necessary to flesh out the controller class – which we just had as a stub earlier.  Update the code with the references to the three new ProcessGuideStep classes.

[WHSWorkExecuteMode(WHSWorkExecuteMode::ValidateAssetScan)]
public class WHSProcessGuideValidateAssetScanController extends ProcessGuideController
{
    protected ProcessGuideStepName initialStepName()
    {
        return classStr(WHSProcessGuideValidateAssetScanContainerIdStep);
    }

    protected ProcessGuideNavigationRoute initializeNavigationRoute()
    {
        ProcessGuideNavigationRoute navigationRoute = new ProcessGuideNavigationRoute();
        navigationRoute.addFollowingStep(classStr(WHSProcessGuideValidateAssetScanContainerIdStep), classStr(WHSProcessGuideValidateAssetScanValidateScanStep));
        navigationRoute.addFollowingStep(classStr(WHSProcessGuideValidateAssetScanValidateScanStep), classStr(WHSProcessGuideValidateAssetScanProcessAssetsStep));
        navigationRoute.addFollowingStep(classStr(WHSProcessGuideValidateAssetScanProcessAssetsStep), classStr(WHSProcessGuideValidateAssetScanContainerIdStep));

        return navigationRoute;
    }

}

This code overrides the two required methods in order to provide a definition of the state machine for this workflow – again using the ProcessGuideStep classes we defined above.  It should be clear how this code is a clear match to the state machine we defined at the beginning of the article – we are defining the specific states and the transition (edges) between them.

Example Workflow

Now that you have seen the code to enable this in a custom workflow, let’s walk through the screens in the mobile app.  You can download the complete code for this project in the link at the bottom of this post – you just need to get it up and running on a dev environment and configure the necessary menu items to enable the workflow for your system.

The initial screen shows the Container ID scanning field.  Note that in the sample project I have included the necessary class to default this to the scanning mode – however you will need to set these up in Dynamics as defined here.

Scanning a container id (CONT-000000001 works if you are in USMF in the Contoso demo data) will navigate you to the next screen and enable the multi-scan Page Pattern.

Here you can enter any number of assets and the app will store them into the local buffer.  As we described above you can view the scanned assets by clicking the icon in the lower left.  After a few scans we would see the UI updated:

Clicking the list icon would show the scans we have performed offline:

Finally clicking the “submit” button on the main screen will push the items to the server, which will then be saved to the custom WHSAsset table and the UI will display the success message.

Conclusion

Hopefully this shows you how to utilize the new ProcessGuide framework for your warehouse customizations.  The code used for this demo is available to download here – please note that this code is demonstration code only and should not be used in a production system without extensive testing.

  • Sorry about the code syntax highlighting – I am still working out the details on how to get X++ code to display correctly. For now it is just using C# formatting.

Posted by Zach in Development, Dynamics

Document Routing Agent Tips

The Document Routing Agent (DRA) is a client-side component for Dynamics that allows customers to connect their local printer resources with Dynamics so that printing from the server-side processes are directed to local devices.  Details here.  We have some customers utilizing very large deployments of these in order to facilitate quick turnaround time – especially in distribution scenarios where label printing timing is critical.

This article is a collection of known issues/workarounds that we have faced that might be valuable to you if you are working with the DRA.

“Key not valid for use in specific state”

This is an error you can hit if the underlying credentials used to connect to Dynamics have changed or expired.  The article posted here describes renaming the entire installation folder here (C:\ProgramData\Microsoft\Microsoft Dynamics 365 for Operations – Document Routing), which is effectively forcing s complete re-install.  Doing so we cause the installation process to create a new Guid for the ApplicationId and all previous printing registration mappings will be lost.  For some deployments this can be hundreds of printer registrations and can be a significant time investment.

You can avoid this re-registering by only removing the TokenCache.dat file instead of the entire directory – this will remove the stored session credentials and allow you to reconfigure them in the app.

Alternatively you can do the complete reinstall and then afterwards update the ida:ApplicationId value in the Microsoft.Dynamics.AX.Framework.DocumentRouting.config config file in the same location.  During the reinstall this value will be regenerated – but you can update the config file with the previously used Guid.


Polling Frequency 

The DRA polls Dynamics every 3 seconds for new documents to print.  This is fine for many of our customers, however large manufacturing and distribution customers often have very tight labeling requirements and expect the timing from scanning of a barcode to label printing to be less than 1 second.  One option I have seen customers adopt here is to install multiple DRA clients – thus you will have multiple polling “windows” and likely reduce the overall wait-time to less than 3 seconds.

Another option is to utilize a “hidden” configuration option in the DRA.  This default value can be overridden by adding the following key to the file: C:\ProgramData\Microsoft\Microsoft Dynamics 365 for Operations – Document Routing\Microsoft.Dynamics.AX.Framework.DocumentRouting.config

<add key=”ida:DocumentRoutingTimerNumSeconds” value=”X” />

This value can be as low as 1 (meaning it will poll every second).

Note that reducing this will have an impact on your Dynamics instance as a new OData call and DB connection will be initiated for every DRA polling event which could cause performance issues.  Be mindful of this and only this this technique if you really have this low printing latency requirements.


Platform Warning

There is a strong connection between the DRA and the platform version used to run Dynamics – the DRA is actually built as part of the platform build.  When you start the DRA after updating Dynamics to a new platform version (after a OneVersion monthly update for example) you will see the following warning message in the DRA:

Many customers view this message as an error and go through the process of reinstalling the DRA during each update cycle.  While this is best practice to ensure compatibility it is not strictly required – the DRA is backwards compatible to all supported platform releases (meaning the previous three monthly releases).  So if you want to avoid updating the DRA each cycle you can simply live with this warning.


Hope these tips were useful – I’ll update with more as I come across other interesting patterns.

Posted by Zach in Dynamics

Microsoft Business Application Summit – 2020

Hello,

Hopefully you were able to join our virtual Microsoft Business Application Summit this year – it was free and open to everyone.  You can access the on-demand content as well.

I did a session on best practices for implementing the Advanced Warehousing module in Dynamics 365 SCM, although much of the core guidance applies to any ERP system.  You can find the session here – and I have included the slides below.  Let me know what you think.

https://mymbas.microsoft.com/sessions/01286481-81c8-4b4d-b702-ae9634ae999f

MBAS WHS Best Practices

Posted by Zach in Dynamics

Warehouse Mobile Devices Portal (WMDP) Posts

Over the past few years I have published several articles about the technical details of Dynamics’ warehouse app, both in terms of architectural overviews as well as customization options.  These were published on an old Dynamics AX SCM blog that no longer exists so they have been moved to a central location.  I am linking to them all here and will be using this blog to post new updates to this series.

Posted by Zach in Dynamics

Hello blog

Hello all,

I know this is sort of old fashion to launch a blog in 2020 – but I wanted a location online to share any interesting discoveries or new features that I might have access to through my work with customers.  I am  a FastTrack Solution Architect for the Dynamics 365 SCM product.  I work with a lot of customers in the manufacturing and distribution space with a focus on warehouse execution.  Feel free to reach out if you have any questions.

Thanks.

Zach

Posted by Zach