Moving to actionable errors

  1. Intro
  2. Error dialog with show-it action
  3. Validation error with fix-it action
  4. Suggestion

Intro

Since BC 22 (runtime 11.0), we can define actionable errors in Microsoft Dynamics 365 Business Central: instead of merely presenting the error message, up to three actions for fixing the error cause can be added to the dialog. And since BC 23 (runtime 12.0), you can add a tooltip to the action. If you are not familiar yet with the look and feel of actionable errors from a user perspective, then start your journey here: Unblocking users with actionable errors.

In order to add actions to error messages, we first need to fill an ErrorInfo variable with the error text and the ErrorInfo.AddAction method, and then use the variable as an alternative parameter for the Error dialog. Let’s take a look at two (partly simplified) examples from the Base App: one for an error dialog with a show-it action, and another one for a validation error with a fix-it action.

Error dialog with show-it action

The following code has been taken from codeunit “Approvals Mgmt.” in BC 23. There are two actions added to the error: the fix-it action “Reject approval request”, and the highlighted show-it action “Show approval comments”, both with a tooltip (4th parameter for AddAction):

    procedure PreventModifyRecIfOpenApprovalEntryExistForCurrentUser(Variant: Variant)
    var
        WorkflowWebhookMgt: Codeunit "Workflow Webhook Management";
        RecRef: RecordRef;
        ErrInfo: ErrorInfo;
        RejectApprovalRequestLbl: Label 'Reject approval';
        ShowCommentsLbl: Label 'Show comments';
        RejectApprovalRequestToolTipLbl: Label 'Reject approval request';
        ShowCommentsToolTipLbl: Label 'Show approval comments';
    begin
        RecRef.GetTable(Variant);
        if HasOpenApprovalEntriesForCurrentUser(RecRef.RecordId) or WorkflowWebhookMgt.HasPendingWorkflowWebhookEntryByRecordId(RecRef.RecordId) then begin
            ErrInfo.ErrorType(ErrorType::Client);
            ErrInfo.Verbosity(Verbosity::Error);
            ErrInfo.Message(PreventModifyRecordWithOpenApprovalEntryMsg);
            ErrInfo.TableId(RecRef.Number);
            ErrInfo.RecordId(RecRef.RecordId);
            ErrInfo.AddAction(RejectApprovalRequestLbl, Codeunit::"Approvals Mgmt.", 'RejectApprovalRequest', RejectApprovalRequestToolTipLbl);
            ErrInfo.AddAction(ShowCommentsLbl, Codeunit::"Approvals Mgmt.", 'ShowApprovalCommentLinesForJournal', ShowCommentsToolTipLbl);
            Error(ErrInfo);
        end;
    end;

There is a Message defined, but not a Title. ErrorType and Verbosity are also set. At the end of line 19, ShowApprovalCommentLinesForJournal from the “Approvals Mgmt.” codeunit is passed as parameter. Let’s check that procedure as well:

    procedure ShowApprovalCommentLinesForJournal(ErrInfo: ErrorInfo)
    var
        ApprovalCommentLine: Record "Approval Comment Line";
        ApprovalComments: Page "Approval Comments";
    begin
        ApprovalCommentLine.SetRange("Table ID", ErrInfo.TableId());
        ApprovalCommentLine.SetRange("Record ID to Approve", ErrInfo.RecordId());
        ApprovalComments.SetTableView(ApprovalCommentLine);
        ApprovalComments.RunModal();
    end;

Validation error with fix-it action

The following code has been taken from table “Sales Line”, field “Qty. to Invoice”.

In BC 22, despite AddAction already being available, the OnValidate trigger still looked like this:

            trigger OnValidate()
            begin
                [...]
                if not InvoiceConditionMet() then // simplified
                    Error(Text005, MaxQtyToInvoice());
                [...]
            end;

Since BC 23, the code looks like this. Mind that here no tooltip is set, probably by mistake. Compared to the first example, we now set a Title, but we are missing ErrorType and Verbosity. When (not) to set these? I honestly don’t know, it apparently also works without:

            trigger OnValidate()
            var
                CannotInvoiceErrorInfo: ErrorInfo;
            begin
                [...]
                if not InvoiceConditionMet() // simplified
                then begin
                    CannotInvoiceErrorInfo.Title := QtyInvoiceNotValidTitleLbl;
                    CannotInvoiceErrorInfo.Message := StrSubstNo(Text005, MaxQtyToInvoice());
                    CannotInvoiceErrorInfo.RecordId := Rec.RecordId;
                    CannotInvoiceErrorInfo.AddAction(StrSubstNo(QtyInvoiceActionLbl, MaxQtyToInvoice()), Codeunit::"Sales Line-Reserve", 'SetSalesQtyInvoice');
                    Error(CannotInvoiceErrorInfo);
                end;
                [...]
            end;

At the end of line 11, SetSalesQtyInvoice from the “Sales Line-Reserve” codeunit is called. The procedure is defined as follows:

    procedure SetSalesQtyInvoice(ErrorInfo: ErrorInfo)
    var
        CurrSalesLine: Record "Sales Line";
    begin
        CurrSalesLine.Get(ErrorInfo.RecordId);
        CurrSalesLine.Validate("Qty. to Invoice", CurrSalesLine.MaxQtyToInvoice());
        CurrSalesLine.Modify(true);
    end;

Suggestion

Looking at the previous validation example, I personally really don’t like how the new ErrorInfo code clutters the OnValidate trigger. Couldn’t we (or: Microsoft) write this in a more elegant way? Here’s my suggestion for a pattern to enhance code organization and readability:

            trigger OnValidate()
            begin
                [...]
                if not InvoiceConditionMet() then
                    Error(CannotInvoiceErrorInfo()); // back to a one-liner
                [...]
            end;

            [...]

            local procedure CannotInvoiceErrorInfo(): ErrorInfo
            var
                ReturnErrorInfo: ErrorInfo;
            begin
                ReturnErrorInfo.Title := QtyInvoiceNotValidTitleLbl;
                ReturnErrorInfo.Message := StrSubstNo(Text005, MaxQtyToInvoice());
                ReturnErrorInfo.RecordId := Rec.RecordId;
                ReturnErrorInfo.AddAction(StrSubstNo(QtyInvoiceActionLbl, MaxQtyToInvoice()), Codeunit::"Sales Line-Reserve", 'SetSalesQtyInvoice');
                exit(ReturnErrorInfo);
             end;

SetSalesQtyInvoice  stays untouched. In the Sales Line table, there are more ErrorInfos to be extracted into procedures. All those new procedures could be gathered in one place. But if the trigger formerly used local variables to fill the ErrorInfo, then of course we would need to convert them as procedure parameters.

Though there still might be some open questions: just try it out – happy coding!

One thought on “Moving to actionable errors

Add yours

Leave a comment

Create a free website or blog at WordPress.com.

Up ↑

Design a site like this with WordPress.com
Get started