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!