How to create a multi-page Wizard in AL

In this video, I show how I created the wizard in the One Field Report. This wizard is created with multiple pages, controlled by a state machine.

https://youtu.be/8aPd9QR_KkE

In this video, Erik walks through how he built a multi-page wizard in AL for Business Central — the same wizard used in his “One Field Report” project. Rather than using the traditional single-page approach with toggled visibility groups, Erik demonstrates a state machine pattern where each wizard step is its own dedicated page object, giving you full control over source tables, layouts, and behavior for every step.

The Traditional Wizard Approach

The standard way to create a wizard in Business Central is to use a single NavigatePage with multiple groups whose visibility is controlled by a step variable. Erik shows a typical setup wizard structure:

  • One page with a NavigatePage type
  • Multiple groups where each group’s Visible property is bound to a condition like Step = 0, Step = 1, Step = 2, etc.
  • A “Next” action that increments the step counter and prepares the next group
  • A “Cancel” action to exit

This approach works well when the wizard is primarily informational — walking users through a setup process with simple fields. But it breaks down when you need fundamentally different content on each step: different source tables, different page layouts, list pages mixed with card pages, or complex interactive controls.

The Problem with Single-Page Wizards

When Erik built the One Field Report wizard, he needed each step to do very different things:

  • A welcome/start screen
  • A step for selecting a table and field (backed by a parameter table)
  • A filter definition page (backed by a filters table)
  • A data verification page (using the Integer virtual table to display arbitrary records)
  • A code/expression editor page
  • Additional list and confirmation pages

Since each step needs its own source table and page structure, cramming everything into a single page with visibility toggles and subpages becomes extremely cumbersome. So Erik went with a completely different architecture.

The Multi-Page State Machine Approach

Instead of one page with hidden groups, Erik created separate page objects for each wizard step (Step 0 through Step 6). Each page is a self-contained NavigatePage with its own source table and layout. The key insight is that when you close one page and open another in Business Central, the UI transition is seamless — there’s no flicker, so it looks and feels exactly like a traditional wizard.

The State Machine

All the wizard flow is controlled by a codeunit with a central procedure called RunTheProcess. This procedure is essentially a state machine implemented as a repeat...until loop:

// Pseudocode representation of the state machine
procedure RunTheProcess(State: Option Start,SelectTable,SelectRecords,VerifyRecords,DefineNewValue,VerifyNewValues,Execute)
var
    Done: Boolean;
begin
    Done := false;
    repeat
        case State of
            State::Start:
                begin
                    // Create a new parameter record
                    // (no UI shown for this state)
                    State := State::SelectTable;
                end;
            State::SelectTable:
                begin
                    Commit();
                    Clear(StepOnePage);
                    StepOnePage.SetParameter(ParameterGuid);
                    StepOnePage.RunModal();
                    if StepOnePage.Continue() then
                        State := State::SelectRecords
                    else
                        Done := true;
                end;
            State::SelectRecords:
                begin
                    Commit();
                    Clear(StepTwoPage);
                    StepTwoPage.SetParameter(ParameterGuid);
                    StepTwoPage.RunModal();
                    if StepTwoPage.Continue() then
                        State := State::VerifyRecords
                    else if StepTwoPage.Back() then
                        State := State::SelectTable
                    else
                        Done := true;
                end;
            // ... same pattern for VerifyRecords, DefineNewValue, VerifyNewValues ...
            State::Execute:
                begin
                    Commit();
                    Clear(StepSixPage);
                    StepSixPage.SetParameter(ParameterGuid);
                    StepSixPage.RunModal();
                    if StepSixPage.Continue() then begin
                        // Execute the actual field changes
                        ExecuteReport();
                        Message('Complete!');
                        Done := true;
                    end else if StepSixPage.Back() then
                        State := State::VerifyNewValues
                    else
                        Done := true;
                end;
        end;
    until Done;
end;

The state is passed in as an option parameter with values: Start, SelectTable, SelectRecords, VerifyRecords, DefineNewValue, VerifyNewValues, and Execute. The loop keeps running until the user either completes the wizard or cancels out.

The Clear() Trick for Re-running Pages

One important technical detail: if you try to call RunModal() on a page variable that has already been run, you’ll get an error saying the object has already been used. The solution is to call Clear() on the page variable before each use. This resets the page object so it can be run again — essential for the back-and-forth navigation in a wizard.

// This pattern is used before every page run:
Commit();
Clear(StepOnePage);                          // Reset the page variable
StepOnePage.SetParameter(ParameterGuid);     // Pass in context
StepOnePage.RunModal();                      // Show the page

Page Communication Pattern

Each wizard page follows the same communication contract with the state machine:

  • SetParameter(Guid) — A procedure that accepts the parameter record’s primary key so the page knows what data to load
  • Continue() — A function that returns whether the Continue button was pressed
  • Back() — A function that returns whether the Back button was pressed (not present on the first step)

Inside each page, these are implemented with global Boolean variables:

// On the wizard page:
var
    ContinuePressed: Boolean;
    BackPressed: Boolean;

procedure Continue(): Boolean
begin
    exit(ContinuePressed);
end;

procedure Back(): Boolean
begin
    exit(BackPressed);
end;

// Continue action
trigger OnAction()
begin
    ContinuePressed := true;
    CurrPage.Close();
end;

// Back action
trigger OnAction()
begin
    BackPressed := true;
    CurrPage.Close();
end;

Controlling the Continue Button

Each page can independently control when the Continue button is enabled. For example, on Step 1 (Select Table and Field), the Continue action’s Enabled property is bound to a CanContinue variable:

// On the page:
var
    CanContinue: Boolean;

// Updated on validate triggers:
trigger OnValidate() // for Table Number field
begin
    CanContinue := (TableNo <> 0) and (FieldNo <> 0);
end;

trigger OnValidate() // for Field Number field  
begin
    CanContinue := (TableNo <> 0) and (FieldNo <> 0);
end;

This means the Continue button only becomes active once the user has selected both a table and a field — a clean validation pattern that’s local to each page.

Three Ways Out

For any given wizard page, there are only three (or four) ways the user can exit:

  1. Continue — Advances to the next state
  2. Back — Returns to the previous state
  3. Cancel / Close (X) — Sets Done := true and exits the wizard entirely
  4. Escape key — Behaves the same as closing the page

If neither Continue nor Back was pressed, the state machine assumes the user wants to exit, and the loop terminates.

Different Source Tables Per Step

One of the biggest advantages of this approach is that each page can have a completely different source table:

  • Step 1 — Source table: Parameter table (for selecting table/field)
  • Step 2 — Source table: Filters table (for defining record filters)
  • Step 3 — Source table: Integer (virtual table for displaying arbitrary records)
  • Step 4 — Source table: Parameter table again (for the expression editor)
  • Step 5 — Source table: Integer (for showing results)
  • Step 6 — Source table: Parameter table (final confirmation)

The Integer table trick is particularly interesting — Erik uses it to display records from any table dynamically, using a MoveRecPointer function that synchronizes the Integer record with a record variable for the actual data table.

What All Pages Have in Common

Despite their differences, every wizard page shares a consistent structure:

  • All are NavigatePage type pages (giving the clean wizard-style bottom action bar)
  • All implement the SetParameter, Continue, and Back communication functions
  • All use the same visual framing with group captions for a consistent look

A Sample AL Extension

While the full wizard code is part of the One Field Report project, here’s a sample AL extension that demonstrates basic dialog and processing patterns in Business Central:

pageextension 50100 CustomerListExt extends "Customer List"
{
    actions
    {
        addfirst(processing)
        {
            action(Test)
            {
                Caption = 'Test';
                ApplicationArea = all;
                trigger OnAction()
                var
                    Window: Dialog;
                    GL: Record "G/L Entry";
                    DialogTextLbl: Label 'Stay calm,\ AL is #1####### #2###### working...';
                    ResultMsg: Label 'Total is %1';
                    i: Integer;
                    Sum: Decimal;
                begin
                    Window.Open(DialogTextLbl);
                    for i := 1 to 50 do begin
                        Window.Update(1, i);
                        if GL.FindSet() then
                            repeat
                                Sum += GL.Amount * Random(3);
                            until GL.Next() = 0;
                        Window.Update(2, Sum);
                    end;
                    Window.Close();
                    Message(ResultMsg, Sum);
                end;
            }
        }
    }
}

Wishlist

Erik mentions one thing he’d love to see from Microsoft: having NavigatePage be a subtype rather than a standalone page type. This would allow combinations like a “Navigate List Page” or a “Navigate Card Page,” giving even more flexibility for wizard step designs.

Summary

The multi-page wizard pattern using a state machine offers significant advantages over the traditional single-page approach when your wizard needs complex, varied content across steps. The key takeaways are:

  • Each wizard step is its own page object — allowing different source tables, layouts, and behaviors
  • A codeunit with a repeat/until loop acts as the state machine — controlling navigation flow between pages
  • Use Clear() on page variables — to allow re-running pages when the user navigates back
  • Pages communicate via simple functionsSetParameter(), Continue(), and Back() with global Boolean variables
  • The UI is seamless — Business Central’s rendering means closing and opening pages produces no visible flicker, making it indistinguishable from a traditional wizard

This is a clean, maintainable pattern that scales well as your wizard grows in complexity. Each page can be developed and tested independently, and the state machine logic remains straightforward to follow and extend.