This is the third in the series of blog posts focusing on the warehouse mobile device portal (WMDP). This portal was released as part of the advanced warehousing solution in Microsoft Dynamics AX 2012 R3 – and we have been trying to explore the powerful ways you have to customize the warehouse workflow for your business. The previous two posts can be found here and here. In this post we will dive deep into customizing and extending the mobile device functionality in order to build new mobile workflows for your users.
Example Workflow Design
To give a better understanding of the design of mobile portal workflows, I thought it would be interesting to create a new workflow and walk through the various changes required to get it up and running in Microsoft Dynamics AX. I have tried to select an example that is simple enough to work through in a blog post, but interesting enough that it will provide some value when you are trying to build your own workflows.The new process we will enable is a “re-weigh” container mobile device workflow. Currently in AX there is no way to weigh a container via the mobile device – but I could imagine a warehouse having a scale at a final inspection point staffed with a mobile device user and capturing the final packed weight. We will create that customization now.
State Machine
We will first create a state machine model that represents the workflow we want to enable for our users. Like any good state machine, this should have clear states and transitions between them. My workflow will be very simple – I will present a screen asking for the ContainerID (assuming the user can scan or enter the ContainerID from a barcode). If this ContainerID is successfully validated we move to the next screen – in which we ask for the weight. If an invalid ContainerID was entered we remain on the same initial screen. When the user enters a valid weight we store the weight in the Container structure and move back to the initial screen. Visually this workflow is depicted below:WHSWorkExecuteDisplay Code
Since this workflow does not directly relate to a Workline, we will be adding it as an “Indirect Mode” MenuItem. See this previous blog for details on Indirect vs. Work Mode. Having this as an Indirect mode item means we will need to add an option to the Activity Code list – which is what is displayed when you select Indirect mode:This list is populated from the WHSWorkActivity enumeration. So our first step will be to add a new enumeration value to this list. We will call the activity “WeighContainer,” with an appropriate label that describes the workflow.
Another enumeration is required to be updated when you are adding any new workflow into the mobile device framework. The WHSWorkExecuteMode enumeration is a superset of the WHSWorkActivity enumeration, as it contains all the Indirect work mode classes as well as all the work creation workflows and work processing workflows. This is essentially the master list of all mobile device work processing workflows. We will need to add a matching enumeration value to this – please ensure the names and labels match, as this is used to convert from one to the other deep within the mobile device framework (I found this out the hard way after debugging for far too long).
I will also add a new enumeration to the system that will represent the states of my state machine diagram. This is not absolutely necessary – but will make reading the code easier.
Once we have these building blocks in place we can add the core processing class. This is the class that will implement the state machine logic and be responsible for constructing the user interface (or coordinating other classes to do it). It must extend the WHSWorkExecuteDisplay base class. We will call ours WHSWorkExecuteDisplayContainerWeight.
The key method to implement is displayForm – as discussed in the previous post this is what is invoked by the framework when requests are mapped to this workflow class. There are many examples in R3 and CU8 of displayForm methods – I have tried to simplify the method and show basic patterns I see as common across many of the classes in the example below:
The basic structure of this method is that it accepts a container with the data passed in from the incoming request, processes this data, and returns a new container with the response to the user. The pass object (which is a member variable of the base WHSWorkExecuteDisplay class) is constructed based on the incoming container. This will be used to test for values that have been stored in previous steps in the workflow.
The test for an error condition is a common pattern – we are checking to see if the user has already been presented with an error message and if so removing the first control from the control collection. This is because when an error is displayed (for example telling the user that they have entered an invalid ContainerId) – it is added to the top of the controls collection. If we are seeing it again then the user is most likely trying to correct the error and if we don’t remove it the error label will persist through to the next screen.
The next section – where I switch based on the current step (which is also a member variable of the base class) is the realization of the state model concept. Depending on exactly what step the user is in determines the UI that is built and/or the processing that needs to occur. This is where we use the new enumeration defined for our state steps. Note the last call is to updateModeStepPass – this is a base class method that will persist any changes to the mode, step, or pass variables which may have changed through the processing of the user request.
Let’s dig into the first step of the process. We have the following methods to support us. Note that I probably could have combined these into a single method, but I wanted to have a separate method for building a “GetContainerId” screen for re-use in the future. If you look at the WHSWorkExecuteDisplay base class you can see many of these helper build* methods (for example buildPick, buildPut, buildGetWorkId, etc).
The buildControl method is used to construct the returned container, populating it with the correct Control container structure. Since this method is so important, let’s talk about each of the parameters.
Name | Data type | Description |
_controlType | str | This tells the framework what type of HTML control to build for the provided data. This must be one of the following macro definitions (defined in WHSRF): #define.RFButton('button')#define.RFLabel('label')#define.RFText('text')#define.RFPassword('password')#define.RFListbox('listbox')#define.RFCombobox('combobox')#define.RFError('error') |
_name | str | Name used for the control in the rendered HTML. Also used as the key to store data persisted in the control within the WHSRFPassthrough Map object. |
_label | str | Text to display to the user in the UI for this control. |
_newLine | int | Field that specifies how this control should be rendered in the UI. If a 1 is passed it means this control should be situated on a new line in the HTML table. If a 0 is passed this control will remain on the same line as the previous control(s). Note this only applied to the default display settings view page used to render the HTML pages. If you have customized the display settings view it is possible this no longer renders in the same way. |
_data | str | Data to display within the control (for example the data to include in a textbox control). Does not apply to label or button controls. |
_inputType | ExtendedTypeId | DataType of the field – important for data entry fields so the correct validation rules can be applied by the framework. For controls that do not require validation the macro #WHSRFUndefinedDataType can be used. |
_error | str | Indicates if this control contains validation errors – used to help the user understand which controls contain validation or input errors information. |
_defaultButton | int | Indicates the specified button should be executed as the default form submit – for example when the user presses the enter key in the UI. This is typically applied to the “OK” button in the forms. |
_enabled | boolean | Indicates if the control should be editable in the UI or if it should be displayed in a read-only mode. Optional – defaults to true. |
_selected | int | If set to 1 this will be the currently active control when the user comes to this page. Optional – defaults to empty string. |
_color | WHSRFColorText | Color to set the control textual data. Optional – defaults to WHSRFColorTest::Default. |
Note also that I use the macro #ContainerId for the name of the control. The included #WHSRF macro contains a long list of available names for all of the control types used in the shipped code – however ContainerId is not one of these options. This will cause us some problems later on when we are trying to process the data from the user. We will need to add #ContainerId and #Weight as available control names to the list of control names in the macro #WHSRF. Here is just a snippet with my additions in the bottom of the list:
Let’s assume the user enters a valid ContainerId and clicks the OK button. This will go through the processing steps discussed above and eventually call our displayForm method with the step value set to 1/WeighContainerStep::EnterWeight (because that is what we set it to after constructing the “GetContainerId” UI). So the getWeightStep method will now need to determine if the ContainerId is valid and keep the user on this step if it is not or display the “GetWeight” UI if it is valid.
The first few lines of this code are a pattern you will see quite often in the codebase for the mobile device. The goal is to take the incoming data from the user and combine it with the existing state data – as well as provide an automatic way of processing the controls on a form and indicate any validation errors. If all goes well the pass Map object will be updated with the data provided by the user and the _ret container will be set to the regenerated container with both the standard controls as well as the user input.
The central if statement checks to ensure we have data entered by the user and does the validation against the WHSContainerTable. Note that we pull out the ContainerId from the pass object – this is another common pattern used to find data used throughout the framework. If the validation is successful the “GetWeight” UI is built and the step variable is advanced to WeighContainerStep::ProcessWeight (2). If the validation fails we display an error to the user, after which we rebuild the “GetContainerId” UI screen.
There is one additional framework change we will need in order to make this code work. Within the WHSRFControlData::processData method the code calls WHSRFControlData::processControl for each of the controls on a form. This method contains specific processing logic for each of the different fields defined in the system. This enables common and consistent processing of fields by the framework each time they are used – but it does mean that when you want to add new field types to the framework this method needs to be expanded. In our case we have added the ContainerId and Weight types – so we need to add the following code to the end of the processControl method:
The final method, processWeightStep, is very similar to the previous – except now when we get the Weight value we update the WHSContainerTable row with the new value. This code can be found in the attached project.
Weigh Container Usage
We now have everything in place for us to utilize the new workflow we created. After creating/importing the code above we will need to ensure to build the IL so the mobile portal can reference the new code. Once this is done we can add the new logic as a Menu Item and add it to the Menu.Menu Item Definition
Adding the new Menu Item to a Menu.
Now we can execute this workflow from the mobile device portal.
Initial screen.
Entering an invalid container ID – remain on the same state with an error message.
Entering a valid ContainerId will move us to the next state – displaying the Weight entry page. Entering a weight on this form and clicking OK will cause us to store the weight in the WHSContainer record and move back to the first state.
Customizing Existing Workflows
While the above discussion is useful for creating entirely new workflows, many customers and partners simply want to augment the existing workflows with new data and/or processes. Microsoft Dynamics AX 2012 R3 ships with all of the code for the WHS workflow processes, and this can be used to decide where exactly to add custom logic and code into the system. This means if there is an existing workflow that implements most of the desired functionality, adding the missing functionality is simply a matter of augmenting the existing code – much like adding customizations into “core” AX logic.For example, you might have a requirement that certain items display a “Fragile” label on the mobile device when directing users to pick them. This could be a special class of items that are deemed especially fragile, which you have added to the production configuration through some sort of customization. Adding this logic to the standard Pick screen is simply a matter of finding the common code that builds the Pick screen and adding new X++ logic to enable this new feature.
Fragile Pick
I have defined the fragile attribute of an item using filter codes within the Warehouse fast tab of the Product information screen. This might not be the best way to define this – I simply wanted an easy way to show the WMDP displaying different UI elements based on the product. My configuration is below:Once this is in place we can look at the WHSWorkExecuteDisplay base class. This class contains many generic builder methods that are used throughout the framework when creating the workflow UI. Fortunately for us there is a buildPick method – which is used by the framework whenever a picking screen is displayed to the user. We will add our code into this method at the very end – to ensure we are not changing any of the code that is used to build the pick screens currently.
In the code above you can see we are checking for the Fragile code – and displaying a new label element in the UI if it is found. Obviously in a production code you would want to use labels for any display strings. If we load this into the browser and perform a pick on a “fragile” item we will see the following UI:
Obviously this is not very attention-grabbing. Fortunately, if you have been following these blog posts about the WMDP you know we can customize the UI through CSS. Let’s modify the default CSS file so as to make this label much more visible. Since we know the name we assigned the new label (FragileLabel) – we can build the css to target this.
This will now display the following – which is much more visible.
Client-Side Notification
It is also be possible to inject code that detects if the fragile label is being displayed and perform additional client-side operations. One common request we hear is to play a beep or perform some sort of haptic feedback to the user to ensure they pay attention to the screen in this sort of situation. While these sorts of customizations are very device-specific and outside the scope of this article, adding the following code to the “mobile device display settings view” page will allow you to write code that would respond to the fragile label being present on the page:Customizing the “mobile device display settings view” page is discussed briefly in this blog post. In the default configuration the above code would be added to the DisplayIEOS.aspx file, located in <AX Installation directory>\6.3\Warehouse Mobile Devices Portal\<WMDP instance folder>\Views\Execute.
The project file containing the code for this example can be found here. This software is provided "as is", without warranty of any kind, express or implied.
No comments:
Post a Comment