JSON References

TL;DR

JSON types reference their value in memory, not the actual value. The below is snipped from https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/jsonobject/jsonobject-data-type

Be careful making JSON types equal to one another. When you do that you copy the reference, not the value. This caught me out.

Example 1

I’m implementing an interface which accepts a JsonObject parameter expecting that you will assign a value which will be used later on. The interface doesn’t require that the JsonObject is passed with var. In fact, it requires that it isn’t. If you include var the compiler will complain that you haven’t implemented all of the interface methods. Something like the JsonExample action in the below code.

“That’s never going to work, the parameter needs to be passed with var” I thought. Better still, just have method return a JsonObject type. However, the interface probably pre-dates complex return types so we’ll let that go. Although, I think you could still return JSON types even before complex return types were introduced…but let it go.

pageextension 50100 "Customer List" extends "Customer List"
{
    actions
    {
        addlast(processing)
        {
            action(JsonExample)
            {
                ApplicationArea = All;

                trigger OnAction()
                var
                    JsonExample: Codeunit "Json Example";
                    Object: JsonObject;
                    Result: Text;
                begin
                    JsonExample.CalcJson(Object);
                    Object.WriteTo(Result);
                    Message(Result);
                end;
            }
            action(JsonExample2)
            {
                ApplicationArea = All;

                trigger OnAction()
                var
                    JsonExample: Codeunit "Json Example";
                    Object: JsonObject;
                    Result: Text;
                begin
                    JsonExample.CalcJson2(Object);
                    Object.WriteTo(Result);
                    Message(Result);
                end;
            }
            action(JsonExample3)
            {
                ApplicationArea = All;

                trigger OnAction()
                var
                    JsonExample: Codeunit "Json Example";
                    Object: JsonObject;
                    Result: Text;
                begin
                    JsonExample.CalcJson3(Object);
                    Object.WriteTo(Result);
                    Message(Result);
                end;
            }
        }
    }
}

codeunit 50100 "Json Example"
{
    procedure CalcJson(Object: JsonObject)
    begin
        Object.Add('aKindOf', 'magic');
    end;

    procedure CalcJson2(Object: JsonObject)
    var
        CalcJson: Codeunit "Calc. Json";
    begin
        Object := CalcJson.CalcJson();
    end;

    procedure CalcJson3(Object: JsonObject)
    var
        CalcJson: Codeunit "Calc. Json";
        JSON: Text;
    begin
        CalcJson.CalcJson().WriteTo(JSON);
        Object.ReadFrom(JSON);
    end;
}

codeunit 50101 "Calc. Json"
{
    procedure CalcJson() Result: JsonObject
    var
        Boys: JsonObject;
    begin
        Boys.Add('backInTown', true);
        Result.Add('boys', Boys);
    end;
}

I was surprised that it did work. Call JsonExample and you get:

{"aKindOf":"magic"}

That’s because even without the var keyword the JsonObject variable holds a refence to the object rather than the value itself, so it still exists after CalcJson() has finished executing.

Example 2

OK, great. I went on to create a separate codeunit to handle the creation of the JsonObject. I wanted to add some error handling and separate the boilerplate of the interface implementation from the business logic.

I wrote something like CalcJson2(). My tests started failing. It seemed that the JsonObject was empty. That puzzled me for a while. What had I done wrong? I think this is the problem.

  1. The JsonObject referenced by the Result variable in codeunit 50101 is created and has the properties added
  2. This reference goes out of scope once CalcJson has finished executing and its value is lost/garbage collected/however it works in Business Central
  3. The JsonObject referenced by the Object parameter is made equal to the first i.e. now points to the first JsonObject in memory – but that value has already gone
  4. As the result the second JsonObject is empty when it is handed back to the calling code

Example 3

Instead of making the JSON types equal to one another explicitly copy the value of one to the other. Like this:

procedure CalcJson3(Object: JsonObject)
var
    CalcJson: Codeunit "Calc. Json";
    JSON: Text;
begin
    CalcJson.CalcJson().WriteTo(JSON);
    Object.ReadFrom(JSON);
end;

In this case writing the value of one to text and then reading it back in to the other. It looks a bit weird, but it works. JsonObject also has a Clone method.

Tip: Install all Apps in a Folder to a Container

Intro

I do this fairly often to prep a new (local) container for development. The script needs to be run on the Docker host and assumes that the PowerShell prompt is already at the folder containing the apps (or apps in child folders) you want to install.

Use the dev endpoint if you are going to want to publish the same apps from VS Code into the container. Or publish into the global scope if you prefer – maybe if you are creating an environment for testing.

Publishing to Dev Scope Using Dev Endpoint

$container = Read-Host -Prompt 'Container'; $credential = Get-Credential; Sort-AppFilesByDependencies -appFiles (gci . -Recurse -Filter *.app | % {$_.FullName}) | % {try{Publish-BcContainerApp $container -appFile $_ -sync -upgrade -skipVerification -useDevEndpoint -credential $credential} catch {Publish-BcContainerApp $container -appFile $_ -sync -install -skipVerification -useDevEndpoint -credential $credential}}

Publishing to Global Scope

$container = Read-Host -Prompt 'Container'; Sort-AppFilesByDependencies -appFiles (gci . -Recurse -Filter *.app | % {$_.FullName}) | % {try{Publish-BcContainerApp $container -appFile $_ -sync -upgrade -skipVerification} catch {Publish-BcContainerApp $container -appFile $_ -sync -install -skipVerification}}

Tip: Get Current Callstack with a Collectible Error

The Code

codeunit 50104 "Get Callstack"
{
    SingleInstance = true;

    [ErrorBehavior(ErrorBehavior::Collect)]
    procedure GetCallstack() Callstack: Text
    var
        LF: Char;
    begin
        LF := 10;
        Error(ErrorInfo.Create('', true));
        Callstack := GetCollectedErrors(true).Get(1).Callstack;
        exit(Callstack.Substring(Callstack.IndexOf(Format(LF)) + 1));
    end;
}

Yea, but…why?

I dunno, I was just curious whether it was possible. And, it is 🧐 Any sensible applications are probably going to be do with error handling or reporting.

You may be tempted to have your code respond differently depending on the context in which it has been called and read the callstack for that purpose. That’s not a train you want to ride though. I’ve tried, it stops at some pretty weird stations.

One advantage of this approach over using a TryFunction (as below) is that the debugger doesn’t break on collectible errors. It can sometimes be frustrating stepping through errors that are always caught to get to the code that you actually want to debug.

procedure LessGoodGetCallstack(): Text
begin
    ThrowError();
    exit(GetLastErrorCallstack());
end;

[TryFunction]
procedure ThrowError()
begin
    Error('');
end;

Part 4: (Slightly) More Elegant Error Handling in Business Central

Collectible Errors

In part 3 we had a look at the new platform feature, collectible errors. Long story short: it makes scenarios where you want to collect and display multiple errors together (e.g. checking journal lines) much easier to code and read, no messing around with if Codeunit.Run then, read the post if you’re interested.

The only thing that let the team down was the horrible user interface to display the errors that have been collected. Shame, but at least the API has been designed with extensibility in mind and we can handle it ourselves. I’m using the same example as in the docs (but with a little added finesse).

GetCollectedErrors

There is a new global method, GetCollectedErrors. This will return a list containing the ErrorInfo objects which have been collected in the current transaction. That gives us all the detail that we need to display the errors in a user-friendlier way.

Show Errors Codeunit

I’ve created a new codeunit to accept the list of errors and show the standard Error Messages page. Of course, you can do whatever you want with this. Create a new table and page to display the errors if you like. For now, the Error Messages page does everything that I need.

See the commit with the changes here if you prefer: https://github.com/jimmymcp/error-message-mgt/commit/89b421db69656b3e2717a0b307975320c3d5ddef

codeunit 50103 "Show Errors"
{
    SingleInstance = true;

    procedure ShowErrors(Errors: List of [ErrorInfo]; Context: Variant)
    var
        TempErrorMsg: Record "Error Message" temporary;
        DataTypeMgt: Codeunit "Data Type Management";
        ErrorInfo: ErrorInfo;
        RecRef: RecordRef;
    begin
        if Errors.Count() = 0 then
            exit;

        foreach ErrorInfo in Errors do begin
            TempErrorMsg.LogDetailedMessage(ErrorInfo.RecordId, ErrorInfo.FieldNo, TempErrorMsg."Message Type"::Error, ErrorInfo.Message, ErrorInfo.DetailedMessage, '');
            if DataTypeMgt.GetRecordRef(Context, RecRef) then
                TempErrorMsg.Validate("Context Record ID", RecRef.RecordId());
            TempErrorMsg.SetErrorCallStack(ErrorInfo.Callstack);
            TempErrorMsg.Modify();
        end;

        Page.Run(Page::"Error Messages", TempErrorMsg);
        Error('');
    end;
}

A couple of notes on the above:

  • There is a SetContext method on the error message record which I’m not using. For reasons I’m not clear about that method sets the context fields on a global variable in the Error Message table, not on the record instance itself
  • Calling the Log methods on the Error Messages table captures the current callstack*, I’m overwriting that with the callstack on the Error Info object
  • The Error(”); on the final line ensures that code execution is stopped and the transaction rolled back. An error like this (outside of If Codeunit.Run then) cannot be collected

I’ve added the last line to the CheckLines method (the method which is collecting the errors):

[ErrorBehavior(ErrorBehavior::Collect)]
local procedure CheckLines(VideoCallBatch: Record "Video Call Batch")
var
    VideoJnlLine: Record "Video Journal Line";
    ShowErrors: Codeunit "Show Errors";
begin
    //check lines code
    ShowErrors.ShowErrors(GetCollectedErrors(), VideoCallBatch.RecordId());
end;

The result is something like this. The Error Messages page does the rest, drilling down to show the callstack, adding an action to open the related record, displaying the name of the field name related to the error.

Error Messages page

Check the docs for more info. The error info API includes IsCollectingErrors(), HasCollectedErrors() and ClearCollectedErrors().

*incidentally, with an ugly hack like this (https://tecman.typepad.com/software_answers/2016/07/context-of-events-in-microsoft-dynamics-nav.html) 😉 I’m not sure whether to be pleased that I wasn’t missing a better way to do it back then or disappointed that there still isn’t a better way to do it 6 years later.**

**Actually, maybe this could be improved with a collectible error?***

leonardo dicaprio dreaming GIF
***thinking about that for too long…

Part 3: (Slightly) More Elegant Error Handling in Business Central

Intro

This is a continuation of a series of posts started around a year ago – you can find the old posts here if you are interested.

Briefly, the goal is this. I’m posting a journal and there are several checks that I need to perform on each line before they are posted. Instead of stopping and showing the first error I want to validate all of the lines and show all of the errors to the user at once.

Previously

The previous posts have been about using the Error Message Management codeunit to achieve that. It works, but I found it clunky. You need to avoid throwing an actual error, but instead collect the problems into a temporary table to display at the end. Then an actual error message is thrown to prevent the journal from being posted.

Problems

What are the problems with that approach?

First, not throwing an error if often harder than throwing one. Instead of simple calls to TestField or Error you need to make corresponding calls to ErrorMessageMgt.LogTestField and ErrorMessageMgt.LogError. In itself that’s OK, but what my be more of a problem is wrapping the posting routine with if Codeunit.Run() then

if not Codeunit.Run(Codeunit::"Posting Routine") then
  ErrorMessageHandler.ShowErrors();

It depends on the context in which you are calling the posting routine. If you are calling it from a page action it’s probably fine. If you are calling it in the middle of some other business logic, not so much.

Second, testing the routine is a bit of a pain. If you want to write an automated test that asserts that when you post the journal line without a posting date then an error is thrown, you can’t. At least not in the way that you expect.

asserterror JournalLine.Post();
Assert.ExpectedError('Posting Date must not be blank');

Something like this won’t work – because an actual TestField error is never thrown. That error message is collected in the temporary table. The only actual message that is thrown is a blank one by the Error Message Handler codeunit. So then you have to either:

  1. complicate your tests by collecting the error messages and asserting that they have the value you expect
  2. complicate your production code with an option to directly throw the errors rather than log them e.g. make sure that you don’t have an active instance of Error Message Management. It isn’t difficult, it just feels messy

Collectible Errors

From BC19 we have the concept of collectible errors. This is quite a different – and better, I think – approach to the same problem. Instead of a framework in AL which we need to dance around avoiding calling Error it is a platform feature which allows us to call Error but tell the platform that the error is collectible and that code can continue to be executed.

The method that the errors are thrown in must indicate that it allows errors to be collected with a new attribute ErrorBehaviour.

[ErrorBehaviour(ErrorBehaviour::Collect)]
local procedure CheckLine(var JournalLine: Record "Journal Line")
begin
  Error(ErrorInfo.Create('Some error message', true));
end;

The Error method has a new overload which takes an ErrorInfo type instead of some text. The ErrorInfo type indicates whether it can be collected or not.

If both the ErrorInfo and the method in which it was thrown are set to allow collection then the error message will not be immediately shown to the user and the code will continue to execute.

Show me the code…

Changes to the Video Journal Batch record

You can view the full changes in this commit: https://github.com/jimmymcp/error-message-mgt/commit/5faa82e614c13017c687d2255305675df0049b29

This is the Post method which is called from the page. You can see the benefits immediately. We can get rid of all that nonsense activating the error message framework and calling if Codeunit.Run. Just call the posting routine and let it do its thing. Much easier to follow.

Next, in the codeunit that handles the batch posting we can get rid of the calls to Error Message Management. I’ve added a new CheckLines method and decorated it with the ErrorBehaviour attribute. This tells the system that any collectible errors which occur within the scope of this method can be collected and code execution can continue.

The level at which we set the ErrorBehaviour attribute is important. I want to continue to check all journal lines in the batch and then stop and show any errors which have been collected. That’s why the ErrorBehaviour is set here – at the journal batch level – rather than at journal line level.

When the system finishes executing the code in this method it will automatically check whether any errors have been collected and show an error message if they have.

Finally, these are the changes to the codeunit which actually checks the journal line. Again, we can ditch the references to the Error Message Management codeunit and replace them with straightforward calls to Error or TestField.

Rather than passing some text with the error message we can pass an ErrorInfo type, returned by ErrorInfo.Create. This is the signature. At a minimum pass the error text, but we also want to indicate that this error can be collected via the collectible parameter. I’m including the instance of Video Journal Line and field number where appropriate as well.

Great to see that TestField has an overload which accepts an ErrorInfo object. The system will fill in the usual error text for you, “<field number> must have a value in <record id>”.

The other parameters are interesting, maybe more about those another time.

procedure Create(Message: Text, [Collectible: Boolean], var [Record: Table], [FieldNo: Integer], [PageNo: Integer], [ControlName: Text], [Verbosity: Verbosity], [DataClassification: DataClassification], [CustomDimensions: Dictionary of [Text, Text]]): ErrorInfo

How does it look?

Put that all together and attempt to post a couple of journal lines which have some validation errors. How does it look?

You get an error dialog as usual. Only this time it says that “Multiple errors occurred during the operation” and gives you the text of the first error message. Click on Detailed information to see a list of all the errors that were collected.

This is what you get.

Kind of underwhelming right?

It was all going so well up to this point but I’ve got a few issues with this:

  1. Given that I’ve gone to the trouble of collecting multiple errors to show to the user all at once it seems counter-intuitive to make the user expand the error to see all the details
  2. Is it just me or is this not easy to read? Once an error message breaks two lines it isn’t obvious how many errors there are. You can’t expand the dialog horizontally either. Even with relatively few errors I’ve had to scroll down to be able to read them all
  3. TestField errors include the record id, which is fine, but for the custom errors I’ve gone to the trouble of giving the record and field number that contains the problem…but that isn’t shown anywhere. I’ve only got 2 lines in my journal in this case, but if I had tens or hundreds it would be really difficult to match the validation errors to the lines that caused them

Custom Handling of Errors

There is a way that we can handle the UI of the error messages ourselves, which is great – and I’ll show an example of that next time. Kudos to the platform team for building that capability in from the start, it’s just a shame that it’s necessary. Call me picky, but I don’t think the standard dialog is really useable.

BC19 CU0

By the way, this doesn’t work properly in BC19 CU0. You have to set the target in app.json to OnPrem – which shouldn’t be necessary. That’s been fixed now.