Recently I was tasked with creating a wizard in Dynamics NAV for a custom claims module. The wizard required collecting some data from the user, which was then used to create a claim entry and some supporting documents (a purchase order or purchase return order). As it turns out, creating a wizard in NAV is quite easy and convenient for the simplest of scenarios.
Unfortunately, I needed to handle 10 different types of claims. All claim types required some basic information. However, the type of claim also determined other information that needed to be collected, which often had some overlap with other claim types.
This left me with two options:
- Create a separate wizard for each claim type.
- Create a single wizard and figure a way to show and hide the steps depending on which claim type I was creating.
I went with option 2, since I didn’t want to eat up 10 page objects.
My first step was to draw out a chart to help visualize how each claim type would flow. There were 7 types of information I required, so I made those each a step. The chart looked like this:
Step | Claim Type 1 | Claim Type 2 | Claim Type 3 | Claim Type 4 | Claim Type 5 | Claim Type 6 | Claim Type 7 | Claim Type 8 | Claim Type 9 | Claim Type 10 |
---|---|---|---|---|---|---|---|---|---|---|
1 | X | X | Done | Done | X | X | X | X | X | X |
2 | Skip | Done | N/A | N/A | Done | Skip | X | Skip | Skip | Skip |
3 | Skip | N/A | N/A | N/A | N/A | X | Done | X | X | Skip |
4 | Skip | N/A | N/A | N/A | N/A | Done | N/A | X | X | Skip |
5 | Skip | N/A | N/A | N/A | N/A | N/A | N/A | Skip | Done | X |
6 | Done | N/A | N/A | N/A | N/A | N/A | N/A | Skip | N/A | Done |
7 | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Done | N/A | N/A |
- X = Get that step’s input
- Done = Claim Type completed on that step
- Skip = Skip input for that step (i.e. don’t display that step)
- N/A = Already completed that claim type in a previous step (i.e. should never reach that step)
So the problem is that we have a wizard that uses next, previous and finish buttons which all do different things dependent on the claim type and the current step. This can lead to some ugly nested if statements.
My initial thought, coming from a .Net background was to use the State Pattern. Unfortunately, this is an object oriented design pattern and is not so NAV friendly (perhaps I could have done it in C# and then exposed some sort of API through NAV/.Net interop, but that seemed like taking a sledgehammer to a finishing nail).
I began some research into alternatives to the State Pattern for imperative languages, which lead me down a rabbit hole of theory behind Finite State Machines, Mealy Machines, Moore Machines and a few other things (I stopped before getting into Turing Machines, but I plan to go back).
During this few hours of digression, I came across State Transition Tables, which looked very similar to my table above. I thought I was on to something. State Transition Tables are usually implemented using a two dimensional array so that you can mimic a table directly in the code. The rows would be the various states or steps (usually represented by an enumeration). As for the columns, one possible implementation could be the current state, the event that triggers a transition, some function that needs to fire as part of the transition and then the target state after the transition is complete. This allows you to see all of the possible transitions directly in the code instead of some separate requirements document. Nice for maintenance down the road.
var transitionTable = new [] { | |
{Step1ClaimType1, Next, ValidateStep1ClaimType1, Step2ClaimType1}, | |
{Step2ClaimType1, Next, ValidateStep2ClaimType1, Finished}, | |
{Step1ClaimType2, Next, ValidateStep1ClaimType2, Step2ClaimType2}, | |
{Step2ClaimType2, Next, ValidateStep2ClaimType2, Step3ClaimType2} | |
// ... | |
// also back and finish events for each type | |
// etc. | |
} |
Dynamics NAV support:
- enumerations (option type) – check
- two-dimensional array – check
- function pointers (or delegates) – fail (as Vjeko has pointed out, .Net does not provide a way for NAV to access this functionality).
Scratch state transition tables.
So as it turns out, you can create a finite state machine in NAV. However, the implementation needs to be done with Case statements. This is a minor improvement over nested if statements, but you are still dealing with mountains of conditional monstrosities. Oh well. You can either nest Case statements (one for the step and one for the claim type in this case), or you can break out the nested part into separate functions, which also helps to eliminate some code duplication, since many claim types overlap.
Here is a breakdown of the basic strategy I took that can be applied to any non-trivial wizard. There is some initialization to get us into step 1 (info required by all claim types), but after that, the flow is generally as follows:
- user provides input for current step
- user triggers a transition event (clicks next)
- enter transition (next) function
- case statement to determine current step
- enter step (x) validation function
- validate input
- enter step (x) transition function
- case statement on claim type
- based on claim type, set current step to appropriate next step
- execute entry actions for new step
- case statement to determine current step
- enter step (x) visibility function
- hide other steps, show current step
- enter function to check if this is the final step for the current claim type
- case statement to determine current claim type
- if final step return yes, otherwise no
- if final step set current step to final step
- enter function to enable/disable buttons
- case statement to determine claim type (or final step)
- enable or disable next/back/finish buttons
- Update page and await user input (go back to the top)
Triggering the back action is similar to the next action, with its own set of equivalent functions.
The finish action is much more straight forward, since you simply create the claim entry based on the gathered data.
While this may not be the ideal implementation, it is repeatable and relatively maintainable. And when you constrain the solution to features available in C/AL, this is the best I could come up with.
Please share any thoughts or alternative implementations in the comments.