If you use AL Test Runner to debug your tests then you are using the OData services to run the test in the background. OData calls have a timeout that is determined by the ODataServicesOperationTimeout key in the service tier configuration. The timeout is set to 8 minutes by default.
This means that a debug session will be closed after 8 minutes and if you haven’t finished you will receive an error like this:
The operation has been canceled because it took longer to generate rows than the specified threshold (00:08:00). Refine your filters to include less data and try again.
Debugging something for more than 8 minutes isn’t a happy place to be – but it happens. You step into a posting routine, the guts of some complex calculations or you aren’t really sure where to start and have to step through loads of code to try to narrow down the field of investigation. That was me this morning.
You can increase the timeout, for longer, blissfully uninterrupted debugging sessions. Yay. You can use the above command (on the Docker host) to set a new value for the timeout in hh:mm:ss. I don’t know if there is a maximum limit on the timeout but if you need more than 20 minutes, as per the example, then you have my sympathy!
Sometimes we want to ask users to make a decision before executing some business logic. Often we just want a yes/no answer and can use a simple Confirm.
But what if you can’t phrase the question to give a yes/no answer? Or if there are more than two options for the user to select between?
Traditionally we’ve used the built-in StrMenu command for that. Think of the menu that you’re given when posting a sales, purchase or warehouse document.
StrMenu is nice and easy to use. Just provide a comma-separated string of the options that you want the user to select between optionally with a default and some instructions and get an integer value back. 0 indicates that the user clicked the Cancel button, otherwise the return value corresponds to the options in the string.
Selection := StrMenu(ShipInvoiceQst, DefaultOption);
Ship := Selection in [1, 3];
Invoice := Selection in [2, 3];
if Selection = 0 then
That’s cool, it works nicely. Let’s not over-complicate things unnecessarily, but you might want to try out an alternative in some situations.
What’s not to love? Give the user some options, get their selection back. Well…
You’ve got no control over the prompt that the user is given. That might be a good thing if you don’t care what the prompt looks like, or might be a problem if you’ve got specific requirements.
It’s difficult to allow someone else to extend. You could have an event before and after the StrMenu I suppose – allow someone to change the string and read the return value. If you’ve got more than one subscriber this is going to get messy pretty quickly though.
Comma-separated values – you should put the values in a label so that it can be translated, but what if you want to use those values more than once? Maintain them and translate them in more than one place? A set of possible values is a first class citizen in AL now that we’ve got enums.
An alternative to StrMenu is to use a confirmation page to display the values defined by an enum. Have that enum implement an interface and you’ve got a better recipe for maintaining your code and allowing others to extend it.
Imagine that you are creating something to allow users to interact with phone numbers. We’ll have some assist-edit or drilldown code on a phone number field and allow the user to take some action.
Initially we decide that the user will be able to either:
Create a new contact and add the phone no. to it
Add the phone no. to an existing contact
Phone No. Action Enum
First, create an enum that defines the available options.
enum 50100 "Phone No. Action"
Extensible = true;
value(0; "Create a new contact")
Caption = 'Create a new contact';
value(1; "Add to existing contact")
Caption = 'Add to existing contact';
I’ve left Extensible = true because I want to allow other devs to add new phone no. capabilities later.
Now a page to present the options in the enum to the user. Because it’s a page we’ve more control over what is displayed and how it works. Add instructional text, add the phone no. for information etc. The PageType is ConfirmationDialog. When you add an enum control to the page it will be rendered as radio buttons rather than the usual dropdown list.
Which gives a page that looks like this. OK, looking good. I’ve also created a codeunit which initialises and calls the page and then retrieves the result.
Having fetched the selected enum value we can call code to create a new contact, have the user select an existing contact or whatever we want.
That’s great, but it could still be better. If another dev adds an enum value it will be displayed on the page correctly but how will their code get called? In a case statement? Should they subscribe to an event? No.
Phone No. Action Interface
We should define an interface that we expect all Phone No. Actions to implement. For now we probably only need one method. I’ll just call it Handle. You could imagine this being more complex in a real example depending on the type of number (Mobile, Landline), country code or some other factors.
Now we make the Phone No. Action enum implement that interface. I’ve created a new codeunit for each action. Those codeunits will also implement the interface and have the guts of the business logic for each action.
enum 50100 "Phone No. Action" implements "Phone No. Action"
Extensible = true;
value(0; "Create a new contact")
Caption = 'Create a new contact';
Implementation = "Phone No. Action" = "Create New Contact";
value(1; "Add to existing contact")
Caption = 'Add to existing contact';
Implementation = "Phone No. Action" = "Add to Exist Contact";
Calling the Confirmation Page and Interface
Finally, this is some sample code that calls the confirmation page to have the user make a selection and then handle their response. Assign the selected action to the interface variable and call its Handle method.
procedure Select(PhoneNo: Text)
SelectAction: Page "Select Phone No. Action";
PhoneNoAction: Interface "Phone No. Action";
if SelectAction.RunModal() = Action::OK then begin
PhoneNoAction := SelectAction.GetResult();
It should be pretty easy to extend this in the future. Let’s say that you want to add an option to call the number or send a message. Just
Extend the enum with the new action
Create a new codeunit which implements the interface and makes the phone call, sends the message or does whatever you need
Link your new enum value with the new codeunit
(there is no step 4, just three easy steps)
Obviously, this is a lot more work than just using StrMenu, especially if some of these concepts are new to you. I’m not going to tell you that you ought to do this or that it is necessarily always worth the extra effort – but it’s good to have the option.
Maybe you’d start with a StrMenu and later refactor it into a confirmation dialog when you know that you’ve got a case for it. Go wild.
Since the introduction of pages, and the deprecation of forms, we have had a fixed set of page types that we can create and a fixed set of controls that we can use on those pages. In the main, that’s a good thing. An appropriate control (text entry, date picker, checkbox etc.) is automatically used depending on the data type of the field, developers need to spend less time on the UI and the page can be automatically adapted to the web client, tablet and phone clients.
If we need finer control over how the page is laid out or want functionality that isn’t supported by the standard controls e.g. drag and drop then we can create a control-add in and use that in a usercontrol on the page instead.
This post isn’t an intro to creating custom control add-ins. There are already good posts out there and I don’t have loads of experience with them anyway.
There is a middle option to consider which might suit simple requirements. We can use the built in control add-ins, including the WebPageViewer.
Simply add a “Microsoft.Dynamics.Nav.Client.WebPageViewer” to the page. Every time I use it Microsoft have added some other capabilities to it – but the methods that we are interested in for now are Navigate and SetContent.
Pretty self-explanatory: Navigate allows you pass a URL that you want the viewer to navigate to. SetContent allows you to set some HTML content that you want to render in the viewer. I’m using this as a way to display a lot of read-only XML like this:
v0.5.0 of AL Test Runner adds some capability to measure code coverage per object and highlight the lines of code that were hit in the previous test run.
This is an example of what we’re aiming for. Running one or more tests, seeing the list of objects and the percentage of their code lines that were hit, opening those objects and highlighting those lines. This should help identify objects and code paths aren’t covered by any tests. I don’t believe code coverage should be a target in itself (maybe more on that in a separate post) but it can be a useful tool to see where you might want to bolster your test suite.
Code coverage is started and stopped before each run of run or more tests. The Test Runner Service app is called to download the code coverage details and they are saved to a JSON file. This file is read and summarised into a per-object percentage which is output with the test results. Only objects which are defined in the workspace are included – so you won’t include standard objects, but you will see test objects.
The path to each object is included so you can Alt+Click to navigate to it. A new Toggle Code Coverage command (Ctrl+Alt+C) allows you to switch the highlighting for lines which have been hit on and off.
Install the Test Runner Service app with the command in Visual Studio Code. If it is already installed you will need to uninstall and unpublish the existing version first
In the AL Test Runner config.json file
Set the path to save the code coverage JSON file to in the codeCoveragePath key. This path is relative to the folder that contains your test code e.g. .//.altestrunner//codecoverage.json to save it within the .altestrunner folder
[Edit: this is now optional – see 0.5.3 update below] Select the path to the code coverage file relative to your app code i.e. if you have your test extension in a separate top level folder you might set it to ../tests/.altestrunner/codecoverage.json This allows AL Test Runner to find and display the code coverage details from an object in your app code
Use the Exclude Code Coverage Files to define file paths that should be excluded from the code coverage summary. This is a regex pattern which is matched against the file paths. For example, setting this to “Tests” will exclude any files with “Tests” in their path.
Test Folder Name – specify the name of the folder which contains the test app. Previously if you worked in a multi-root workspace and had an editor open at the production app it would attempt to run tests in the production app, create a config file, ask you which company to test in, prompt for credentials…meh. With this setting AL Test Runner will always run tests in the app which is contained in the folder with the name given in this setting.
Some of the early feedback from people who were trying to enable code coverage was that it was a bit of a game. And not in a good, fun way. More like Ludo. You’re trying to line up all your pieces but every time you think you’ve got them where you want them someone else lands on them and messes everything up.
From 0.5.3 it isn’t necessary to set the code coverage path in VS Code’s settings (see setup #3 above). If this path is not set then the extension will attempt to find the codecoverage.json file somewhere in the workspace.
The codeCoveragePath key in the AL Test Runner config file is still required, but has a default value which will be fine in most cases.
Ideas and feedback welcome as issues (or better yet, pull requests) in the repo on GitHub. These are some that I might work on.
Maybe a setting to enter a glob pattern to include/exclude certain files and/or paths from the summary
Smoother setup of the different settings that are required – I’ve tried to provide sensible default values but they are a few things to enter correctly to get it working
Currently the code coverage file is overwritten with each test run – it would be more useful if this was cumulative so that running a single test didn’t overwrite all the results from a previous test run. If you want a complete overview you have to run the whole suite – but then maybe that isn’t a bad thing
Perhaps an overall code coverage percentage for the whole project as well as per app (with the above caveat that I’m sceptical about fixed code coverage targets)
A CodeLens at the top of an object with its percentage code coverage that toggles the highlighting on/off when clicked
I’ve been pretty quiet on the blogging front for the last few months – settling in to a new team and a new role.
For the first post of 2021 what better topic than Item Tracking? Let’s be honest, the Item Tracking Lines page is a mess. Over 3,000 lines of code, tonnes of global variables and more than 60 methods. I don’t think the fundamental design of the page has changed much since it was introduced – it’s a bullet that no one wants to bite.
To be fair, making radical changes to the design would be very disruptive to the base application and I guess to lots of solutions and bespoke that have been written on top of it. So instead the page now has a layer of 44 (yes, forty four) events on top of it – not including the built-in page events. Now we are going to add another layer of code on top and try not to knock the whole jenga tower down in the process.
I’m sorry, that’s harsh. The point of this post isn’t really to criticise the design of item tracking – it is what it is – but share a few things I’ve found trying to extend it.
Item Tracking Lines makes extensive use of global variables which can be a problem when you are trying to change the behaviour of the page. Some of the variables are shared in the protected var section.
As these variables are protected (rather than local) they can be accessed by a page extension for the Item Tracking Lines page. You can just refer directly to the variable and use it in your page extension code.
You’ll notice that all the variables are xyzVisible and xyzEditable which control whether you can see and/or edit various controls on the page.
You can get access to some more of the important global variables on the page with the GetVariables method. There is a corresponding SetVariables method which allows you to override the Tracking Specification records to insert, modify or delete.
If you want to override the value of other global variables on the page then you will have to hunt through the list of events. As there are so many events on the page its likely that there is going to be one that allows you to handle the calculation of the variable that you are interested in.
Calling Page Methods
As noted above, a lot of business logic exists in the Item Tracking Lines page itself. Some important functions are in the Item Tracking Management and Item Tracking Data Collection codeunits but it is likely that you are going to need to call one or more of the methods on the page.
I wanted to avoid having all my code in the Item Tracking Lines page extension and instead split it out into one or more codeunits to handle my specific functionality.
OK, but if my code needs to call the methods on the page itself, how am I going to be able to do that from code inside a codeunit?
Passing the Current Instance of the Page to a Codeunit
Easy, I thought. I’ll just pass the current instance of the page as a parameter to my codeunit. Declare a parameter of type Page of the Item Tracking Line page, pass it by reference with the var keyword – Robert is your mother’s brother.
procedure FunkyItemTracking(var TrackingSpecification: Record "Tracking Specification" temporary; var ItemTrackingLines: Page "Item Tracking Lines")
//some funky code
Erm…no. How do you get the current instance of a page? With CurrPage. CurrPage is an object of type CurrPage not Page. Which gives the following compilation error: “Cannot convert from CurrPage “Item Tracking Lines” to var Page “Item Tracking Lines”.
There is another way. An event can IncludeSender which passes the calling object along with any other parameters a subscriber.
I don’t want anyone else to subscribe to this event and mess about with it. For that, we can use the InternalEvent attribute. From the docs: “Specifies that the method is published as an internal event. It can only be subscribed to from within the same module.”
You can create a new internal event on your Item Tracking Lines page extension, including the sender and subscribe to it in a separate codeunit. That way you’ve got an instance of the page whose methods you can call while keeping your code separated. Have and eat your cake.
I’ve also manually bound my subscribing codeunit. That way I can have multiple subscribers in the codeunit and maintain some state in its variables.
//Item Tracking Lines page extension
FunkyItemTracking: Codeunit "Funky Item Tracking";
local procedure OnCallingFunkyItemTracking(var TempTrackingSpecification: "Tracking Specification" temporary)
local procedure FunkyItemTracking(var TempTrackingSpecification: Record "Tracking Specification" temporary; var ItemTrackingLines: Page "Item Tracking Lines")
//some funky item tracking customisation
[EventSubscriber(ObjectType::Page, Page::"Item Tracking Lines", 'OnCallingFunkyItemTracking', '', false, false)]
local procedure OnCallingFunkyItemTracking(var Sender: Page "Item Tracking Lines"; var TempTrackingSpecification: Record "Tracking Specification" temporary)
I’ve reused the same concept the simplifying retrieving variable values. If you want to read the source quantity array, undefined quantity array, the Item record or Tracking Specification records to insert, modify or delete you’ll need to call the page’s GetVariables method.
That’s fine – but that method uses 10 var parameters to retrieve the variables. If you’re only trying to get at the value of one of those variables – or in my case, one element of one the arrays – it’s just a bit messy to create 10 variables that you don’t even need.
//Item Tracking Lines page extensions
//just define the variable that you are interested in, keeps your code easier to read
local procedure OnGetUndefinedQty(var UndefinedQty: Decimal)
[EventSubscriber(ObjectType::Page, Page::"Item Tracking Lines", 'OnGetUndefinedQty', '', false, false)]
local procedure OnGetUndefinedQty(var Sender: Page "Item Tracking Lines"; var UndefinedQty: Decimal)
TempTrackingSpecInsert, TempTrackingSpecModify, TempTrackingSpecDelete : Record "Tracking Specification" temporary;
Item: Record Item;
UndefinedQtyArray: Array of Decimal;
SourceQuantityArray: Array of Decimal;
InsertIsBlocked, DeleteIsBlocked, BlockCommit : Boolean;
Sender.GetVariables(TempTrackingSpecInsert, TempTrackingSpecModify, TempTrackingSpecDelete, Item, UndefinedQtyArray, SourceQuantityArray, CurrentSignFactor, InsertIsBlocked, DeleteIsBlocked, BlockCommit);
UndefinedQty := UndefinedQtyArray;
There’s a lot more to say about the Item Tracking Lines page, but I’ll leave it there for now. Maybe somebody will find this interesting and/or useful.